mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Refactoring [skip ci]
This commit is contained in:
parent
61b95461d5
commit
6a548e11a6
@ -20,7 +20,6 @@ swift_library(
|
|||||||
"//submodules/Postbox:Postbox",
|
"//submodules/Postbox:Postbox",
|
||||||
"//submodules/TelegramCore:TelegramCore",
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
||||||
"//submodules/MeshAnimationCache:MeshAnimationCache",
|
|
||||||
"//submodules/Utils/RangeSet:RangeSet",
|
"//submodules/Utils/RangeSet:RangeSet",
|
||||||
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
||||||
"//submodules/TextFormat:TextFormat",
|
"//submodules/TextFormat:TextFormat",
|
||||||
|
@ -10,7 +10,6 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import DeviceLocationManager
|
import DeviceLocationManager
|
||||||
import TemporaryCachedPeerDataManager
|
import TemporaryCachedPeerDataManager
|
||||||
import MeshAnimationCache
|
|
||||||
import InAppPurchaseManager
|
import InAppPurchaseManager
|
||||||
import AnimationCache
|
import AnimationCache
|
||||||
import MultiAnimationRenderer
|
import MultiAnimationRenderer
|
||||||
@ -935,7 +934,6 @@ public protocol AccountContext: AnyObject {
|
|||||||
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
|
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
|
||||||
|
|
||||||
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
|
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
|
||||||
var meshAnimationCache: MeshAnimationCache { get }
|
|
||||||
|
|
||||||
var animationCache: AnimationCache { get }
|
var animationCache: AnimationCache { get }
|
||||||
var animationRenderer: MultiAnimationRenderer { get }
|
var animationRenderer: MultiAnimationRenderer { get }
|
||||||
|
@ -95,8 +95,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? {
|
public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManagerPlayerType? {
|
||||||
func extractFileMedia(_ message: Message) -> TelegramMediaFile? {
|
func extractFileMedia(_ message: EngineMessage) -> TelegramMediaFile? {
|
||||||
var file: TelegramMediaFile?
|
var file: TelegramMediaFile?
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let media = media as? TelegramMediaFile {
|
if let media = media as? TelegramMediaFile {
|
||||||
@ -120,7 +120,7 @@ public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayer
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
|
public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
|
||||||
if isGlobalSearch && !isDownloadList {
|
if isGlobalSearch && !isDownloadList {
|
||||||
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
|
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
|
||||||
} else if isRecentActions && !isDownloadList {
|
} else if isRecentActions && !isDownloadList {
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -371,7 +370,7 @@ private final class DayComponent: Component {
|
|||||||
private var currentSelection: DaySelection?
|
private var currentSelection: DaySelection?
|
||||||
|
|
||||||
private(set) var timestamp: Int32?
|
private(set) var timestamp: Int32?
|
||||||
private(set) var index: MessageIndex?
|
private(set) var index: EngineMessage.Index?
|
||||||
private var isHighlightingEnabled: Bool = false
|
private var isHighlightingEnabled: Bool = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -983,12 +982,12 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
|
|
||||||
private weak var controller: CalendarMessageScreen?
|
private weak var controller: CalendarMessageScreen?
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peerId: PeerId
|
private let peerId: EnginePeer.Id
|
||||||
private let initialTimestamp: Int32
|
private let initialTimestamp: Int32
|
||||||
private let enableMessageRangeDeletion: Bool
|
private let enableMessageRangeDeletion: Bool
|
||||||
private let canNavigateToEmptyDays: Bool
|
private let canNavigateToEmptyDays: Bool
|
||||||
private let navigateToOffset: (Int, Int32) -> Void
|
private let navigateToOffset: (Int, Int32) -> Void
|
||||||
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private var scrollView: Scroller
|
private var scrollView: Scroller
|
||||||
@ -1019,13 +1018,13 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
init(
|
init(
|
||||||
controller: CalendarMessageScreen,
|
controller: CalendarMessageScreen,
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
peerId: PeerId,
|
peerId: EnginePeer.Id,
|
||||||
calendarSource: SparseMessageCalendar,
|
calendarSource: SparseMessageCalendar,
|
||||||
initialTimestamp: Int32,
|
initialTimestamp: Int32,
|
||||||
enableMessageRangeDeletion: Bool,
|
enableMessageRangeDeletion: Bool,
|
||||||
canNavigateToEmptyDays: Bool,
|
canNavigateToEmptyDays: Bool,
|
||||||
navigateToOffset: @escaping (Int, Int32) -> Void,
|
navigateToOffset: @escaping (Int, Int32) -> Void,
|
||||||
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
) {
|
) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -1783,9 +1782,9 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
guard let calendarState = self.calendarState else {
|
guard let calendarState = self.calendarState else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var messageMap: [Message] = []
|
var messageMap: [EngineMessage] = []
|
||||||
for (_, entry) in calendarState.messagesByDay {
|
for (_, entry) in calendarState.messagesByDay {
|
||||||
messageMap.append(entry.message)
|
messageMap.append(EngineMessage(entry.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedMedia: [Int: [Int: DayMedia]] = [:]
|
var updatedMedia: [Int: [Int: DayMedia]] = [:]
|
||||||
@ -1805,7 +1804,7 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
mediaLoop: for media in message.media {
|
mediaLoop: for media in message.media {
|
||||||
switch media {
|
switch media {
|
||||||
case _ as TelegramMediaImage, _ as TelegramMediaFile:
|
case _ as TelegramMediaImage, _ as TelegramMediaFile:
|
||||||
updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media))
|
updatedMedia[i]![day] = DayMedia(message: message, media: EngineMedia(media))
|
||||||
break mediaLoop
|
break mediaLoop
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@ -1830,13 +1829,13 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peerId: PeerId
|
private let peerId: EnginePeer.Id
|
||||||
private let calendarSource: SparseMessageCalendar
|
private let calendarSource: SparseMessageCalendar
|
||||||
private let initialTimestamp: Int32
|
private let initialTimestamp: Int32
|
||||||
private let enableMessageRangeDeletion: Bool
|
private let enableMessageRangeDeletion: Bool
|
||||||
private let canNavigateToEmptyDays: Bool
|
private let canNavigateToEmptyDays: Bool
|
||||||
private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void
|
private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void
|
||||||
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
|
|
||||||
@ -1844,13 +1843,13 @@ public final class CalendarMessageScreen: ViewController {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
peerId: PeerId,
|
peerId: EnginePeer.Id,
|
||||||
calendarSource: SparseMessageCalendar,
|
calendarSource: SparseMessageCalendar,
|
||||||
initialTimestamp: Int32,
|
initialTimestamp: Int32,
|
||||||
enableMessageRangeDeletion: Bool,
|
enableMessageRangeDeletion: Bool,
|
||||||
canNavigateToEmptyDays: Bool,
|
canNavigateToEmptyDays: Bool,
|
||||||
navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void,
|
navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void,
|
||||||
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
|
@ -3,7 +3,6 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
@ -17,6 +16,26 @@ import ConfettiEffect
|
|||||||
import TelegramUniversalVideoContent
|
import TelegramUniversalVideoContent
|
||||||
import SolidRoundedButtonNode
|
import SolidRoundedButtonNode
|
||||||
|
|
||||||
|
private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? {
|
||||||
|
if useTotalFileAllocatedSize {
|
||||||
|
let url = URL(fileURLWithPath: path)
|
||||||
|
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) {
|
||||||
|
if values.isRegularFile ?? false {
|
||||||
|
if let fileSize = values.totalFileAllocatedSize {
|
||||||
|
return Int64(fileSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = stat()
|
||||||
|
if stat(path, &value) == 0 {
|
||||||
|
return value.st_size
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class ProgressEstimator {
|
private final class ProgressEstimator {
|
||||||
private var averageProgressPerSecond: Double = 0.0
|
private var averageProgressPerSecond: Double = 0.0
|
||||||
private var lastMeasurement: (Double, Float)?
|
private var lastMeasurement: (Double, Float)?
|
||||||
@ -91,7 +110,7 @@ private final class ImportManager {
|
|||||||
return self.statePromise.get()
|
return self.statePromise.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
init(account: Account, peerId: EnginePeer.Id, mainFile: EngineTempBox.File, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.archivePath = archivePath
|
self.archivePath = archivePath
|
||||||
self.entries = entries
|
self.entries = entries
|
||||||
@ -234,8 +253,8 @@ private final class ImportManager {
|
|||||||
|
|
||||||
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
|
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
|
||||||
|
|
||||||
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
|
let unpackedFile = Signal<EngineTempBox.File, ImportError> { subscriber in
|
||||||
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
|
let tempFile = EngineTempBox.shared.tempFile(fileName: entry.0.path)
|
||||||
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
|
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
|
||||||
let startTime = CACurrentMediaTime()
|
let startTime = CACurrentMediaTime()
|
||||||
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
|
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
|
||||||
@ -440,9 +459,9 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
|
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
|
||||||
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
|
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
|
||||||
|
|
||||||
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
|
||||||
|
|
||||||
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
|
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
|
||||||
|
|
||||||
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
|
||||||
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
|
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
|
||||||
@ -724,9 +743,9 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
fileprivate let cancel: () -> Void
|
fileprivate let cancel: () -> Void
|
||||||
fileprivate var peerId: PeerId
|
fileprivate var peerId: EnginePeer.Id
|
||||||
private let archivePath: String?
|
private let archivePath: String?
|
||||||
private let mainEntry: TempBoxFile
|
private let mainEntry: EngineTempBox.File
|
||||||
private let totalBytes: Int64
|
private let totalBytes: Int64
|
||||||
private let totalMediaBytes: Int64
|
private let totalMediaBytes: Int64
|
||||||
private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]
|
private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]
|
||||||
@ -746,7 +765,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: EnginePeer.Id, archivePath: String?, mainEntry: EngineTempBox.File, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.cancel = cancel
|
self.cancel = cancel
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
@ -818,7 +837,7 @@ public final class ChatImportActivityScreen: ViewController {
|
|||||||
self.progressEstimator = ProgressEstimator()
|
self.progressEstimator = ProgressEstimator()
|
||||||
self.beganCompletion = false
|
self.beganCompletion = false
|
||||||
|
|
||||||
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
|
let resolvedPeerId: Signal<EnginePeer.Id, ImportManager.ImportError>
|
||||||
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||||
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|
||||||
|> mapError { _ -> ImportManager.ImportError in
|
|> mapError { _ -> ImportManager.ImportError in
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
@ -30,8 +29,8 @@ private final class ChatListFilterPresetControllerArguments {
|
|||||||
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
||||||
let openAddIncludePeer: () -> Void
|
let openAddIncludePeer: () -> Void
|
||||||
let openAddExcludePeer: () -> Void
|
let openAddExcludePeer: () -> Void
|
||||||
let deleteIncludePeer: (PeerId) -> Void
|
let deleteIncludePeer: (EnginePeer.Id) -> Void
|
||||||
let deleteExcludePeer: (PeerId) -> Void
|
let deleteExcludePeer: (EnginePeer.Id) -> Void
|
||||||
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
|
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
|
||||||
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
|
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
|
||||||
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
|
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
|
||||||
@ -49,8 +48,8 @@ private final class ChatListFilterPresetControllerArguments {
|
|||||||
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
|
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
|
||||||
openAddIncludePeer: @escaping () -> Void,
|
openAddIncludePeer: @escaping () -> Void,
|
||||||
openAddExcludePeer: @escaping () -> Void,
|
openAddExcludePeer: @escaping () -> Void,
|
||||||
deleteIncludePeer: @escaping (PeerId) -> Void,
|
deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
|
||||||
deleteExcludePeer: @escaping (PeerId) -> Void,
|
deleteExcludePeer: @escaping (EnginePeer.Id) -> Void,
|
||||||
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
|
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
|
||||||
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
|
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
|
||||||
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
|
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
|
||||||
@ -93,7 +92,7 @@ private enum ChatListFilterPresetControllerSection: Int32 {
|
|||||||
|
|
||||||
private enum ChatListFilterPresetEntryStableId: Hashable {
|
private enum ChatListFilterPresetEntryStableId: Hashable {
|
||||||
case index(Int)
|
case index(Int)
|
||||||
case peer(PeerId)
|
case peer(EnginePeer.Id)
|
||||||
case includePeerInfo
|
case includePeerInfo
|
||||||
case excludePeerInfo
|
case excludePeerInfo
|
||||||
case includeCategory(ChatListFilterIncludeCategory)
|
case includeCategory(ChatListFilterIncludeCategory)
|
||||||
@ -311,7 +310,7 @@ private extension ChatListFilterCategoryIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private enum ChatListFilterRevealedItemId: Equatable {
|
private enum ChatListFilterRevealedItemId: Equatable {
|
||||||
case peer(PeerId)
|
case peer(EnginePeer.Id)
|
||||||
case includeCategory(ChatListFilterIncludeCategory)
|
case includeCategory(ChatListFilterIncludeCategory)
|
||||||
case excludeCategory(ChatListFilterExcludeCategory)
|
case excludeCategory(ChatListFilterExcludeCategory)
|
||||||
}
|
}
|
||||||
@ -573,8 +572,8 @@ private struct ChatListFilterPresetControllerState: Equatable {
|
|||||||
var excludeMuted: Bool
|
var excludeMuted: Bool
|
||||||
var excludeRead: Bool
|
var excludeRead: Bool
|
||||||
var excludeArchived: Bool
|
var excludeArchived: Bool
|
||||||
var additionallyIncludePeers: [PeerId]
|
var additionallyIncludePeers: [EnginePeer.Id]
|
||||||
var additionallyExcludePeers: [PeerId]
|
var additionallyExcludePeers: [EnginePeer.Id]
|
||||||
|
|
||||||
var revealedItemId: ChatListFilterRevealedItemId?
|
var revealedItemId: ChatListFilterRevealedItemId?
|
||||||
var expandedSections: Set<FilterSection>
|
var expandedSections: Set<FilterSection>
|
||||||
@ -825,7 +824,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var includePeers: [PeerId] = []
|
var includePeers: [EnginePeer.Id] = []
|
||||||
for peerId in peerIds {
|
for peerId in peerIds {
|
||||||
switch peerId {
|
switch peerId {
|
||||||
case let .peer(id):
|
case let .peer(id):
|
||||||
@ -838,7 +837,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
|||||||
|
|
||||||
if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks {
|
if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks {
|
||||||
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
|
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
|
||||||
var removedPeers: [PeerId] = []
|
var removedPeers: [EnginePeer.Id] = []
|
||||||
if let data = filter.data {
|
if let data = filter.data {
|
||||||
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
|
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
|
||||||
}
|
}
|
||||||
@ -951,7 +950,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludePeers: [PeerId] = []
|
var excludePeers: [EnginePeer.Id] = []
|
||||||
for peerId in peerIds {
|
for peerId in peerIds {
|
||||||
switch peerId {
|
switch peerId {
|
||||||
case let .peer(id):
|
case let .peer(id):
|
||||||
@ -1144,7 +1143,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
|||||||
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id)))
|
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
|
let currentPeers = Atomic<[EnginePeer.Id: EngineRenderedPeer]>(value: [:])
|
||||||
let stateWithPeers = statePromise.get()
|
let stateWithPeers = statePromise.get()
|
||||||
|> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in
|
|> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in
|
||||||
let currentPeersValue = currentPeers.with { $0 }
|
let currentPeersValue = currentPeers.with { $0 }
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
@ -39,7 +38,7 @@ private enum ChatListFilterPresetListSection: Int32 {
|
|||||||
case list
|
case list
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
|
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
|
||||||
if peers.isEmpty {
|
if peers.isEmpty {
|
||||||
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
|
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import ItemListUI
|
import ItemListUI
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -665,13 +665,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
nodeInteraction?.openPasswordSetup()
|
nodeInteraction?.openPasswordSetup()
|
||||||
case .premiumUpgrade, .premiumAnnualDiscount:
|
case .premiumUpgrade, .premiumAnnualDiscount:
|
||||||
nodeInteraction?.openPremiumIntro()
|
nodeInteraction?.openPremiumIntro()
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.openChatFolderUpdates()
|
|
||||||
}
|
}
|
||||||
case .hide:
|
case .hide:
|
||||||
switch notice {
|
switch notice {
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.hideChatFolderUpdates()
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -966,13 +962,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
nodeInteraction?.openPasswordSetup()
|
nodeInteraction?.openPasswordSetup()
|
||||||
case .premiumUpgrade, .premiumAnnualDiscount:
|
case .premiumUpgrade, .premiumAnnualDiscount:
|
||||||
nodeInteraction?.openPremiumIntro()
|
nodeInteraction?.openPremiumIntro()
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.openChatFolderUpdates()
|
|
||||||
}
|
}
|
||||||
case .hide:
|
case .hide:
|
||||||
switch notice {
|
switch notice {
|
||||||
case .chatFolderUpdates:
|
|
||||||
nodeInteraction?.hideChatFolderUpdates()
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -210,8 +210,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
|||||||
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||||
|
|
||||||
switch item.notice {
|
switch item.notice {
|
||||||
case .chatFolderUpdates:
|
|
||||||
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.ChatList_HideAction, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
|
|
||||||
default:
|
default:
|
||||||
strongSelf.setRevealOptions((left: [], right: []))
|
strongSelf.setRevealOptions((left: [], right: []))
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import UniversalMediaPlayer
|
import UniversalMediaPlayer
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
|
||||||
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus?, NoError> {
|
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus?, NoError> {
|
||||||
guard let playerType = peerMessageMediaPlayerType(message) else {
|
guard let playerType = peerMessageMediaPlayerType(message) else {
|
||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
@ -21,7 +20,7 @@ private func internalMessageFileMediaPlaybackStatus(context: AccountContext, fil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus, NoError> {
|
public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus, NoError> {
|
||||||
var duration = 0.0
|
var duration = 0.0
|
||||||
if let value = file.duration {
|
if let value = file.duration {
|
||||||
duration = Double(value)
|
duration = Double(value)
|
||||||
@ -33,7 +32,7 @@ public func messageFileMediaPlaybackStatus(context: AccountContext, file: Telegr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<Float, NoError> {
|
public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<Float, NoError> {
|
||||||
guard let playerType = peerMessageMediaPlayerType(message) else {
|
guard let playerType = peerMessageMediaPlayerType(message) else {
|
||||||
return .never()
|
return .never()
|
||||||
}
|
}
|
||||||
@ -45,7 +44,7 @@ public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, fi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
|
public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
|
||||||
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) |> map { status -> MediaPlayerPlaybackStatus? in
|
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) |> map { status -> MediaPlayerPlaybackStatus? in
|
||||||
return status?.status
|
return status?.status
|
||||||
}
|
}
|
||||||
@ -99,7 +98,7 @@ public func messageFileMediaResourceStatus(context: AccountContext, file: Telegr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func messageImageMediaResourceStatus(context: AccountContext, image: TelegramMediaImage, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
|
public func messageImageMediaResourceStatus(context: AccountContext, image: TelegramMediaImage, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
|
||||||
if message.flags.isSending {
|
if message.flags.isSending {
|
||||||
return combineLatest(messageMediaImageStatus(context: context, messageId: message.id, image: image), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|
return combineLatest(messageMediaImageStatus(context: context, messageId: message.id, image: image), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|
||||||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||||
|
@ -37,13 +37,13 @@ private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, med
|
|||||||
switch block {
|
switch block {
|
||||||
case let .image(id, caption, _, _):
|
case let .image(id, caption, _, _):
|
||||||
if let m = media[id] {
|
if let m = media[id] {
|
||||||
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
|
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: EngineMedia(m), url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
|
||||||
counter += 1
|
counter += 1
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case let .video(id, caption, _, _):
|
case let .video(id, caption, _, _):
|
||||||
if let m = media[id] {
|
if let m = media[id] {
|
||||||
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
|
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: EngineMedia(m), url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
|
||||||
counter += 1
|
counter += 1
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -82,7 +82,7 @@ public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galle
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: galleryMedia, url: nil, caption: nil, credit: nil), caption: nil, credit: nil, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0)
|
result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: EngineMedia(galleryMedia), url: nil, caption: nil, credit: nil), caption: nil, credit: nil, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0 ..< result.count {
|
for i in 0 ..< result.count {
|
||||||
@ -123,7 +123,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||||||
if case .suggestedProfilePhoto = action.action {
|
if case .suggestedProfilePhoto = action.action {
|
||||||
isSuggested = true
|
isSuggested = true
|
||||||
}
|
}
|
||||||
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false, nil)])
|
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false, nil)])
|
||||||
|
|
||||||
let sourceCorners: AvatarGalleryController.SourceCorners
|
let sourceCorners: AvatarGalleryController.SourceCorners
|
||||||
if case .photoUpdated = action.action {
|
if case .photoUpdated = action.action {
|
||||||
@ -131,7 +131,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
|||||||
} else {
|
} else {
|
||||||
sourceCorners = .round
|
sourceCorners = .round
|
||||||
}
|
}
|
||||||
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in
|
let galleryController = AvatarGalleryController(context: context, peer: peer, sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in
|
||||||
|
|
||||||
})
|
})
|
||||||
return .chatAvatars(galleryController, image)
|
return .chatAvatars(galleryController, image)
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramBaseController
|
import TelegramBaseController
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Vision
|
import Vision
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import AccountContext
|
import AccountContext
|
||||||
@ -27,8 +26,8 @@ private final class CachedImageRecognizedContent: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId) -> Signal<CachedImageRecognizedContent?, NoError> {
|
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal<CachedImageRecognizedContent?, NoError> {
|
||||||
let key = ValueBoxKey(length: 20)
|
let key = EngineDataBuffer(length: 20)
|
||||||
key.setInt32(0, value: messageId.namespace)
|
key.setInt32(0, value: messageId.namespace)
|
||||||
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
||||||
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
|
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
|
||||||
@ -40,8 +39,8 @@ private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: Mes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> {
|
private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> {
|
||||||
let key = ValueBoxKey(length: 20)
|
let key = EngineDataBuffer(length: 20)
|
||||||
key.setInt32(0, value: messageId.namespace)
|
key.setInt32(0, value: messageId.namespace)
|
||||||
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
||||||
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
|
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
|
||||||
@ -333,7 +332,7 @@ private func recognizeContent(in image: UIImage?) -> Signal<[RecognizedContent],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: MessageId) -> Signal<[RecognizedContent], NoError> {
|
public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: EngineMessage.Id) -> Signal<[RecognizedContent], NoError> {
|
||||||
if context.sharedContext.immediateExperimentalUISettings.disableImageContentAnalysis {
|
if context.sharedContext.immediateExperimentalUISettings.disableImageContentAnalysis {
|
||||||
return .single([])
|
return .single([])
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
enum StickerVerificationStatus {
|
enum StickerVerificationStatus {
|
||||||
@ -74,7 +73,7 @@ public class ImportStickerPack {
|
|||||||
let emojis: [String]
|
let emojis: [String]
|
||||||
let keywords: String
|
let keywords: String
|
||||||
let uuid: UUID
|
let uuid: UUID
|
||||||
var resource: MediaResource?
|
var resource: EngineMediaResource?
|
||||||
|
|
||||||
init(content: Content, emojis: [String], keywords: String, uuid: UUID = UUID()) {
|
init(content: Content, emojis: [String], keywords: String, uuid: UUID = UUID()) {
|
||||||
self.content = content
|
self.content = content
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
@ -87,23 +86,23 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var signals: [Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError>] = []
|
var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = []
|
||||||
for sticker in strongSelf.stickerPack.stickers {
|
for sticker in strongSelf.stickerPack.stickers {
|
||||||
if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] {
|
if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] {
|
||||||
signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType)
|
signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource._asResource(), alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType)
|
||||||
|> map { result -> (UUID, StickerVerificationStatus, MediaResource?) in
|
|> map { result -> (UUID, StickerVerificationStatus, EngineMediaResource?) in
|
||||||
switch result {
|
switch result {
|
||||||
case .progress:
|
case .progress:
|
||||||
return (sticker.uuid, .loading, nil)
|
return (sticker.uuid, .loading, nil)
|
||||||
case let .complete(resource, mimeType):
|
case let .complete(resource, mimeType):
|
||||||
if ["application/x-tgsticker", "video/webm"].contains(mimeType) {
|
if ["application/x-tgsticker", "video/webm"].contains(mimeType) {
|
||||||
return (sticker.uuid, .verified, resource)
|
return (sticker.uuid, .verified, EngineMediaResource(resource))
|
||||||
} else {
|
} else {
|
||||||
return (sticker.uuid, .declined, nil)
|
return (sticker.uuid, .declined, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> `catch` { _ -> Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError> in
|
|> `catch` { _ -> Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError> in
|
||||||
return .single((sticker.uuid, .declined, nil))
|
return .single((sticker.uuid, .declined, nil))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -115,7 +114,7 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
|
|||||||
}
|
}
|
||||||
var verifiedStickers = Set<UUID>()
|
var verifiedStickers = Set<UUID>()
|
||||||
var declinedStickers = Set<UUID>()
|
var declinedStickers = Set<UUID>()
|
||||||
var uploadedStickerResources: [UUID: MediaResource] = [:]
|
var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
|
||||||
for (uuid, result, resource) in results {
|
for (uuid, result, resource) in results {
|
||||||
switch result {
|
switch result {
|
||||||
case .verified:
|
case .verified:
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
@ -52,8 +51,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private var stickerPack: ImportStickerPack?
|
private var stickerPack: ImportStickerPack?
|
||||||
var stickerResources: [UUID: MediaResource] = [:]
|
var stickerResources: [UUID: EngineMediaResource] = [:]
|
||||||
private var uploadedStickerResources: [UUID: MediaResource] = [:]
|
private var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
|
||||||
private var stickerPackReady = true
|
private var stickerPackReady = true
|
||||||
|
|
||||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||||
@ -623,11 +622,11 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
|||||||
}
|
}
|
||||||
if let resource = self.uploadedStickerResources[item.stickerItem.uuid] {
|
if let resource = self.uploadedStickerResources[item.stickerItem.uuid] {
|
||||||
if let localResource = item.stickerItem.resource {
|
if let localResource = item.stickerItem.resource {
|
||||||
self.context.account.postbox.mediaBox.copyResourceData(from: localResource.id, to: resource.id)
|
self.context.account.postbox.mediaBox.copyResourceData(from: localResource._asResource().id, to: resource._asResource().id)
|
||||||
}
|
}
|
||||||
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
|
stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
|
||||||
} else if let resource = item.stickerItem.resource {
|
} else if let resource = item.stickerItem.resource {
|
||||||
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
|
stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var thumbnailSticker: ImportSticker?
|
var thumbnailSticker: ImportSticker?
|
||||||
@ -695,23 +694,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
|||||||
let context = strongSelf.context
|
let context = strongSelf.context
|
||||||
|
|
||||||
Queue.mainQueue().after(1.0) {
|
Queue.mainQueue().after(1.0) {
|
||||||
var firstItem: StickerPackItem?
|
let firstItem: StickerPackItem? = firstStickerItem?.stickerPackItem
|
||||||
if let firstStickerItem = firstStickerItem, let resource = firstStickerItem.resource as? TelegramMediaResource {
|
|
||||||
var fileAttributes: [TelegramMediaFileAttribute] = []
|
|
||||||
if firstStickerItem.mimeType == "video/webm" {
|
|
||||||
fileAttributes.append(.FileName(fileName: "sticker.webm"))
|
|
||||||
fileAttributes.append(.Animated)
|
|
||||||
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
|
|
||||||
} else if firstStickerItem.mimeType == "application/x-tgsticker" {
|
|
||||||
fileAttributes.append(.FileName(fileName: "sticker.tgs"))
|
|
||||||
fileAttributes.append(.Animated)
|
|
||||||
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
|
|
||||||
} else {
|
|
||||||
fileAttributes.append(.FileName(fileName: "sticker.webp"))
|
|
||||||
}
|
|
||||||
fileAttributes.append(.ImageSize(size: firstStickerItem.dimensions))
|
|
||||||
firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: firstStickerItem.mimeType, size: nil, attributes: fileAttributes), indexKeys: [])
|
|
||||||
}
|
|
||||||
strongSelf.presentInGlobalOverlay?(UndoOverlayController(presentationData: strongSelf.presentationData, content: .stickersModified(title: strongSelf.presentationData.strings.StickerPackActionInfo_AddedTitle, text: strongSelf.presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: firstItem ?? items.first, context: strongSelf.context), elevatedLayout: false, action: { action in
|
strongSelf.presentInGlobalOverlay?(UndoOverlayController(presentationData: strongSelf.presentationData, content: .stickersModified(title: strongSelf.presentationData.strings.StickerPackActionInfo_AddedTitle, text: strongSelf.presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: firstItem ?? items.first, context: strongSelf.context), elevatedLayout: false, action: { action in
|
||||||
if case .info = action {
|
if case .info = action {
|
||||||
(navigationController?.viewControllers.last as? ViewController)?.present(StickerPackScreen(context: context, mode: .settings, mainStickerPack: .id(id: info.id.id, accessHash: info.accessHash), stickerPacks: [], parentNavigationController: navigationController, actionPerformed: { _ in
|
(navigationController?.viewControllers.last as? ViewController)?.present(StickerPackScreen(context: context, mode: .settings, mainStickerPack: .id(id: info.id.id, accessHash: info.accessHash), stickerPacks: [], parentNavigationController: navigationController, actionPerformed: { _ in
|
||||||
@ -800,7 +783,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStickerPack(_ stickerPack: ImportStickerPack, verifiedStickers: Set<UUID>, declinedStickers: Set<UUID>, uploadedStickerResources: [UUID: MediaResource]) {
|
func updateStickerPack(_ stickerPack: ImportStickerPack, verifiedStickers: Set<UUID>, declinedStickers: Set<UUID>, uploadedStickerResources: [UUID: EngineMediaResource]) {
|
||||||
self.stickerPack = stickerPack
|
self.stickerPack = stickerPack
|
||||||
self.uploadedStickerResources = uploadedStickerResources
|
self.uploadedStickerResources = uploadedStickerResources
|
||||||
var updatedItems: [StickerPackPreviewGridEntry] = []
|
var updatedItems: [StickerPackPreviewGridEntry] = []
|
||||||
@ -813,8 +796,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
|||||||
} else {
|
} else {
|
||||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data)
|
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data)
|
||||||
item.resource = resource
|
item.resource = EngineMediaResource(resource)
|
||||||
self.stickerResources[item.uuid] = resource
|
self.stickerResources[item.uuid] = EngineMediaResource(resource)
|
||||||
}
|
}
|
||||||
var isInitiallyVerified = false
|
var isInitiallyVerified = false
|
||||||
if case .image = item.content {
|
if case .image = item.content {
|
||||||
|
@ -3,7 +3,6 @@ import UIKit
|
|||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
@ -4,7 +4,6 @@ import Display
|
|||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import StickerResources
|
import StickerResources
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import AnimatedStickerNode
|
import AnimatedStickerNode
|
||||||
@ -142,7 +141,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
|||||||
if case .video = stickerItem.content {
|
if case .video = stickerItem.content {
|
||||||
isVideo = true
|
isVideo = true
|
||||||
}
|
}
|
||||||
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource._asResource(), isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
|
||||||
}
|
}
|
||||||
animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import StickerResources
|
import StickerResources
|
||||||
@ -87,7 +86,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
|||||||
if case .video = item.content {
|
if case .video = item.content {
|
||||||
isVideo = true
|
isVideo = true
|
||||||
}
|
}
|
||||||
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource._asResource(), isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
|
||||||
}
|
}
|
||||||
self.animationNode?.visibility = true
|
self.animationNode?.visibility = true
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import Postbox
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import PersistentStringHash
|
import PersistentStringHash
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AccountContext
|
import AccountContext
|
||||||
import UrlHandling
|
import UrlHandling
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -20,11 +19,11 @@ public final class InstantPageArticleItem: InstantPageItem {
|
|||||||
let contentSize: CGSize
|
let contentSize: CGSize
|
||||||
let cover: TelegramMediaImage?
|
let cover: TelegramMediaImage?
|
||||||
let url: String
|
let url: String
|
||||||
let webpageId: MediaId
|
let webpageId: EngineMedia.Id
|
||||||
let rtl: Bool
|
let rtl: Bool
|
||||||
let hasRTL: Bool
|
let hasRTL: Bool
|
||||||
|
|
||||||
init(frame: CGRect, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool, hasRTL: Bool) {
|
init(frame: CGRect, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, rtl: Bool, hasRTL: Bool) {
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
self.userLocation = userLocation
|
self.userLocation = userLocation
|
||||||
self.webPage = webPage
|
self.webPage = webPage
|
||||||
@ -73,7 +72,7 @@ public final class InstantPageArticleItem: InstantPageItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func layoutArticleItem(theme: InstantPageTheme, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: MediaId, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem {
|
func layoutArticleItem(theme: InstantPageTheme, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem {
|
||||||
let inset: CGFloat = 17.0
|
let inset: CGFloat = 17.0
|
||||||
let imageSpacing: CGFloat = 10.0
|
let imageSpacing: CGFloat = 10.0
|
||||||
var sideInset = inset
|
var sideInset = inset
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -20,14 +19,14 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
|
|||||||
private var imageNode: TransformImageNode?
|
private var imageNode: TransformImageNode?
|
||||||
|
|
||||||
let url: String
|
let url: String
|
||||||
let webpageId: MediaId
|
let webpageId: EngineMedia.Id
|
||||||
let cover: TelegramMediaImage?
|
let cover: TelegramMediaImage?
|
||||||
|
|
||||||
private let openUrl: (InstantPageUrlItem) -> Void
|
private let openUrl: (InstantPageUrlItem) -> Void
|
||||||
|
|
||||||
private var fetchedDisposable = MetaDisposable()
|
private var fetchedDisposable = MetaDisposable()
|
||||||
|
|
||||||
init(context: AccountContext, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, openUrl: @escaping (InstantPageUrlItem) -> Void) {
|
init(context: AccountContext, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, openUrl: @escaping (InstantPageUrlItem) -> Void) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.url = url
|
self.url = url
|
||||||
self.webpageId = webpageId
|
self.webpageId = webpageId
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
@ -36,7 +35,7 @@ private func generatePauseButton(color: UIColor) -> UIImage? {
|
|||||||
|
|
||||||
private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString {
|
private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString {
|
||||||
let string = NSMutableAttributedString()
|
let string = NSMutableAttributedString()
|
||||||
if let file = media.media as? TelegramMediaFile {
|
if case let .file(file) = media.media {
|
||||||
loop: for attribute in file.attributes {
|
loop: for attribute in file.attributes {
|
||||||
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
|
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
|
||||||
let titleText: String = title ?? strings.MediaPlayer_UnknownTrack
|
let titleText: String = title ?? strings.MediaPlayer_UnknownTrack
|
||||||
@ -101,7 +100,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
|
|||||||
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5), chapters: []))
|
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5), chapters: []))
|
||||||
|
|
||||||
let playlistType: MediaManagerPlayerType
|
let playlistType: MediaManagerPlayerType
|
||||||
if let file = self.media.media as? TelegramMediaFile {
|
if case let .file(file) = self.media.media {
|
||||||
playlistType = file.isVoice ? .voice : .music
|
playlistType = file.isVoice ? .voice : .music
|
||||||
} else {
|
} else {
|
||||||
playlistType = .music
|
playlistType = .music
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -555,7 +555,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
if item is InstantPageWebEmbedItem {
|
if item is InstantPageWebEmbedItem {
|
||||||
embedIndex += 1
|
embedIndex += 1
|
||||||
}
|
}
|
||||||
if let imageItem = item as? InstantPageImageItem, imageItem.media.media is TelegramMediaWebpage {
|
if let imageItem = item as? InstantPageImageItem, case .webpage = imageItem.media.media {
|
||||||
embedIndex += 1
|
embedIndex += 1
|
||||||
}
|
}
|
||||||
if item is InstantPageDetailsItem {
|
if item is InstantPageDetailsItem {
|
||||||
@ -1003,17 +1003,17 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
private func longPressMedia(_ media: InstantPageMedia) {
|
private func longPressMedia(_ media: InstantPageMedia) {
|
||||||
let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
||||||
if let strongSelf = self, let image = media.media as? TelegramMediaImage {
|
if let strongSelf = self, case let .image(image) = media.media {
|
||||||
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||||
let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
|
let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
|
||||||
}
|
}
|
||||||
}), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in
|
}), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in
|
||||||
if let strongSelf = self, let image = media.media as? TelegramMediaImage {
|
if let strongSelf = self, case let .image(image) = media.media {
|
||||||
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||||
let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
|
let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
|
||||||
}
|
}
|
||||||
}), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
}), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
||||||
if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage {
|
if let strongSelf = self, let webPage = strongSelf.webPage, case let .image(image) = media.media {
|
||||||
strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil)
|
strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil)
|
||||||
}
|
}
|
||||||
})], catchTapsOutside: true)
|
})], catchTapsOutside: true)
|
||||||
@ -1406,7 +1406,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let map = media.media as? TelegramMediaMap {
|
if case let .geo(map) = media.media {
|
||||||
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
|
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
|
||||||
}, stopLiveLocation: { _ in
|
}, stopLiveLocation: { _ in
|
||||||
}, openUrl: { _ in }, openPeer: { _ in
|
}, openUrl: { _ in }, openPeer: { _ in
|
||||||
@ -1420,12 +1420,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let file = media.media as? TelegramMediaFile, (file.isVoice || file.isMusic) {
|
if case let .file(file) = media.media, (file.isVoice || file.isMusic) {
|
||||||
var medias: [InstantPageMedia] = []
|
var medias: [InstantPageMedia] = []
|
||||||
var initialIndex = 0
|
var initialIndex = 0
|
||||||
for item in items {
|
for item in items {
|
||||||
for itemMedia in item.medias {
|
for itemMedia in item.medias {
|
||||||
if let itemFile = itemMedia.media as? TelegramMediaFile, (itemFile.isVoice || itemFile.isMusic) {
|
if case let .file(itemFile) = itemMedia.media, (itemFile.isVoice || itemFile.isMusic) {
|
||||||
if itemMedia.index == media.index {
|
if itemMedia.index == media.index {
|
||||||
initialIndex = medias.count
|
initialIndex = medias.count
|
||||||
}
|
}
|
||||||
@ -1440,16 +1440,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
var fromPlayingVideo = false
|
var fromPlayingVideo = false
|
||||||
|
|
||||||
var entries: [InstantPageGalleryEntry] = []
|
var entries: [InstantPageGalleryEntry] = []
|
||||||
if media.media is TelegramMediaWebpage {
|
if case let .webpage(webPage) = media.media {
|
||||||
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
|
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
|
||||||
} else if let file = media.media as? TelegramMediaFile, file.isAnimated {
|
} else if case let .file(file) = media.media, file.isAnimated {
|
||||||
fromPlayingVideo = true
|
fromPlayingVideo = true
|
||||||
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
|
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
|
||||||
} else {
|
} else {
|
||||||
fromPlayingVideo = true
|
fromPlayingVideo = true
|
||||||
var medias: [InstantPageMedia] = mediasFromItems(items)
|
var medias: [InstantPageMedia] = mediasFromItems(items)
|
||||||
medias = medias.filter {
|
medias = medias.filter { item in
|
||||||
return $0.media is TelegramMediaImage || $0.media is TelegramMediaFile
|
switch item.media {
|
||||||
|
case .image, .file:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for media in medias {
|
for media in medias {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -96,9 +96,9 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let image = self.media.media as? TelegramMediaImage {
|
if case let .image(image) = self.media.media {
|
||||||
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
|
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
|
||||||
} else if let file = self.media.media as? TelegramMediaFile {
|
} else if case let .file(file) = self.media.media {
|
||||||
if file.isVideo {
|
if file.isVideo {
|
||||||
var indexData: GalleryItemIndexData?
|
var indexData: GalleryItemIndexData?
|
||||||
if let location = self.location {
|
if let location = self.location {
|
||||||
@ -122,7 +122,7 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||||||
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||||
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
|
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
|
||||||
}
|
}
|
||||||
} else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content {
|
} else if case let .webpage(embedWebpage) = self.media.media, case let .Loaded(webpageContent) = embedWebpage.content {
|
||||||
if webpageContent.url.hasSuffix(".m3u8") {
|
if webpageContent.url.hasSuffix(".m3u8") {
|
||||||
let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), userLocation: userLocation, content: .url(webpageContent.url), streamVideo: true, loopVideo: false)
|
let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), userLocation: userLocation, content: .url(webpageContent.url), streamVideo: true, loopVideo: false)
|
||||||
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, { makeArguments, navigationController, present in
|
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, { makeArguments, navigationController, present in
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -43,7 +42,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
|
|
||||||
private var currentSize: CGSize?
|
private var currentSize: CGSize?
|
||||||
|
|
||||||
private var fetchStatus: MediaResourceStatus?
|
private var fetchStatus: EngineMediaResource.FetchStatus?
|
||||||
private var fetchedDisposable = MetaDisposable()
|
private var fetchedDisposable = MetaDisposable()
|
||||||
private var statusDisposable = MetaDisposable()
|
private var statusDisposable = MetaDisposable()
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
|
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
|
||||||
self.addSubnode(self.pinchContainerNode)
|
self.addSubnode(self.pinchContainerNode)
|
||||||
|
|
||||||
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
|
||||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
|
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
|
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
displayLinkDispatcher.dispatch {
|
displayLinkDispatcher.dispatch {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.fetchStatus = status
|
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
|
||||||
strongSelf.updateFetchStatus()
|
strongSelf.updateFetchStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,7 +104,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
|
|
||||||
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
||||||
}
|
}
|
||||||
} else if let file = media.media as? TelegramMediaFile {
|
} else if case let .file(file) = media.media {
|
||||||
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
|
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
|
||||||
if file.mimeType.hasPrefix("image/") {
|
if file.mimeType.hasPrefix("image/") {
|
||||||
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
|
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
|
||||||
@ -119,7 +118,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
||||||
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
|
||||||
}
|
}
|
||||||
} else if let map = media.media as? TelegramMediaMap {
|
} else if case let .geo(map) = media.media {
|
||||||
self.addSubnode(self.pinNode)
|
self.addSubnode(self.pinNode)
|
||||||
|
|
||||||
var dimensions = CGSize(width: 200.0, height: 100.0)
|
var dimensions = CGSize(width: 200.0, height: 100.0)
|
||||||
@ -131,7 +130,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
}
|
}
|
||||||
let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height))
|
let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height))
|
||||||
self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource))
|
self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource))
|
||||||
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image {
|
} else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image {
|
||||||
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||||
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
|
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
|
||||||
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
|
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
|
||||||
@ -211,7 +210,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
if self.currentSize != size || self.themeUpdated {
|
if self.currentSize != size || self.themeUpdated {
|
||||||
self.currentSize = size
|
self.currentSize = size
|
||||||
self.themeUpdated = false
|
self.themeUpdated = false
|
||||||
|
|
||||||
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
self.pinchContainerNode.update(size: size, transition: .immediate)
|
self.pinchContainerNode.update(size: size, transition: .immediate)
|
||||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
@ -219,7 +218,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
let radialStatusSize: CGFloat = 50.0
|
let radialStatusSize: CGFloat = 50.0
|
||||||
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
|
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
|
||||||
|
|
||||||
if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
|
||||||
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
|
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
|
||||||
let boundingSize = size
|
let boundingSize = size
|
||||||
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
|
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
|
||||||
@ -228,15 +227,15 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
apply()
|
apply()
|
||||||
|
|
||||||
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
|
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
|
||||||
} else if let file = self.media.media as? TelegramMediaFile, let dimensions = file.dimensions {
|
} else if case let .file(file) = self.media.media, let dimensions = file.dimensions {
|
||||||
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
|
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
|
||||||
|
|
||||||
let imageSize = dimensions.cgSize.aspectFilled(size)
|
let imageSize = dimensions.cgSize.aspectFilled(size)
|
||||||
let boundingSize = size
|
let boundingSize = size
|
||||||
let makeLayout = self.imageNode.asyncLayout()
|
let makeLayout = self.imageNode.asyncLayout()
|
||||||
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor))
|
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor))
|
||||||
apply()
|
apply()
|
||||||
} else if self.media.media is TelegramMediaMap {
|
} else if case .geo = self.media.media {
|
||||||
for attribute in self.attributes {
|
for attribute in self.attributes {
|
||||||
if let mapAttribute = attribute as? InstantPageMapAttribute {
|
if let mapAttribute = attribute as? InstantPageMapAttribute {
|
||||||
let imageSize = mapAttribute.dimensions.aspectFilled(size)
|
let imageSize = mapAttribute.dimensions.aspectFilled(size)
|
||||||
@ -254,7 +253,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
let (pinSize, pinApply) = makePinLayout(self.context, theme, .location(nil))
|
let (pinSize, pinApply) = makePinLayout(self.context, theme, .location(nil))
|
||||||
self.pinNode.frame = CGRect(origin: CGPoint(x: floor((size.width - pinSize.width) / 2.0), y: floor(size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize)
|
self.pinNode.frame = CGRect(origin: CGPoint(x: floor((size.width - pinSize.width) / 2.0), y: floor(size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize)
|
||||||
pinApply()
|
pinApply()
|
||||||
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) {
|
} else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) {
|
||||||
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
|
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
|
||||||
let boundingSize = size
|
let boundingSize = size
|
||||||
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
|
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
|
||||||
@ -290,7 +289,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
case .Local:
|
case .Local:
|
||||||
switch gesture {
|
switch gesture {
|
||||||
case .tap:
|
case .tap:
|
||||||
if self.media.media is TelegramMediaImage && self.media.index == -1 {
|
if case .image = self.media.media, self.media.index == -1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openMedia(self.media)
|
self.openMedia(self.media)
|
||||||
@ -311,7 +310,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
|||||||
} else {
|
} else {
|
||||||
switch gesture {
|
switch gesture {
|
||||||
case .tap:
|
case .tap:
|
||||||
if self.media.media is TelegramMediaImage && self.media.index == -1 {
|
if case .image = self.media.media, self.media.index == -1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openMedia(self.media)
|
self.openMedia(self.media)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
@ -48,7 +47,7 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout {
|
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout {
|
||||||
|
|
||||||
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
|
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
|
||||||
var items: [InstantPageItem] = []
|
var items: [InstantPageItem] = []
|
||||||
@ -373,7 +372,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
contentSize.height += verticalInset
|
contentSize.height += verticalInset
|
||||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||||
case let .image(id, caption, url, webpageId):
|
case let .image(id, caption, url, webpageId):
|
||||||
if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
if case let .image(image) = media[id], let largest = largestImageRepresentation(image.representations) {
|
||||||
let imageSize = largest.dimensions
|
let imageSize = largest.dimensions
|
||||||
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
|
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
|
||||||
|
|
||||||
@ -397,7 +396,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
|
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: image, url: mediaUrl, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
|
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .image(image), url: mediaUrl, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
|
||||||
|
|
||||||
items.append(mediaItem)
|
items.append(mediaItem)
|
||||||
contentSize.height += filledSize.height
|
contentSize.height += filledSize.height
|
||||||
@ -413,7 +412,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
||||||
}
|
}
|
||||||
case let .video(id, caption, autoplay, _):
|
case let .video(id, caption, autoplay, _):
|
||||||
if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions {
|
if case let .file(file) = media[id], let dimensions = file.dimensions {
|
||||||
let imageSize = dimensions
|
let imageSize = dimensions
|
||||||
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
|
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
|
||||||
|
|
||||||
@ -433,11 +432,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
var items: [InstantPageItem] = []
|
var items: [InstantPageItem] = []
|
||||||
|
|
||||||
if autoplay {
|
if autoplay {
|
||||||
let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: caption.text, credit: caption.credit), interactive: true)
|
let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: caption.text, credit: caption.credit), interactive: true)
|
||||||
|
|
||||||
items.append(mediaItem)
|
items.append(mediaItem)
|
||||||
} else {
|
} else {
|
||||||
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
|
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
|
||||||
|
|
||||||
items.append(mediaItem)
|
items.append(mediaItem)
|
||||||
}
|
}
|
||||||
@ -460,11 +459,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
var size = CGSize()
|
var size = CGSize()
|
||||||
switch subItem {
|
switch subItem {
|
||||||
case let .image(id, _, _, _):
|
case let .image(id, _, _, _):
|
||||||
if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
|
if case let .image(image) = media[id], let largest = largestImageRepresentation(image.representations) {
|
||||||
size = largest.dimensions.cgSize
|
size = largest.dimensions.cgSize
|
||||||
}
|
}
|
||||||
case let .video(id, _, _, _):
|
case let .video(id, _, _, _):
|
||||||
if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions {
|
if case let .file(file) = media[id], let dimensions = file.dimensions {
|
||||||
size = dimensions.cgSize
|
size = dimensions.cgSize
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -502,9 +501,15 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
var items: [InstantPageItem] = []
|
var items: [InstantPageItem] = []
|
||||||
|
|
||||||
if !author.isEmpty {
|
if !author.isEmpty {
|
||||||
let avatar: TelegramMediaImage? = avatarId.flatMap { media[$0] as? TelegramMediaImage }
|
let avatar: TelegramMediaImage? = avatarId.flatMap { id -> TelegramMediaImage? in
|
||||||
|
if case let .image(image) = media[id] {
|
||||||
|
return image
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if let avatar = avatar {
|
if let avatar = avatar {
|
||||||
let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: avatar, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: true, fit: false)
|
let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: .image(avatar), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: true, fit: false)
|
||||||
items.append(avatarItem)
|
items.append(avatarItem)
|
||||||
|
|
||||||
avatarInset += 62.0
|
avatarInset += 62.0
|
||||||
@ -572,7 +577,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
for subBlock in subItems {
|
for subBlock in subItems {
|
||||||
switch subBlock {
|
switch subBlock {
|
||||||
case let .image(id, caption, url, webpageId):
|
case let .image(id, caption, url, webpageId):
|
||||||
if let image = media[id] as? TelegramMediaImage, let imageSize = largestImageRepresentation(image.representations)?.dimensions {
|
if case let .image(image) = media[id], let imageSize = largestImageRepresentation(image.representations)?.dimensions {
|
||||||
let mediaIndex = mediaIndexCounter
|
let mediaIndex = mediaIndexCounter
|
||||||
mediaIndexCounter += 1
|
mediaIndexCounter += 1
|
||||||
|
|
||||||
@ -583,7 +588,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
if let url = url {
|
if let url = url {
|
||||||
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
|
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
|
||||||
}
|
}
|
||||||
itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, url: mediaUrl, caption: caption.text, credit: caption.credit))
|
itemMedias.append(InstantPageMedia(index: mediaIndex, media: .image(image), url: mediaUrl, caption: caption.text, credit: caption.credit))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@ -626,11 +631,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
var contentSize: CGSize
|
var contentSize: CGSize
|
||||||
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
|
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
|
||||||
let item: InstantPageItem
|
let item: InstantPageItem
|
||||||
if let url = url, let coverId = coverId, let image = media[coverId] as? TelegramMediaImage {
|
if let url = url, let coverId = coverId, case let .image(image) = media[coverId] {
|
||||||
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil)
|
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil)
|
||||||
let content = TelegramMediaWebpageContent.Loaded(loadedContent)
|
let content = TelegramMediaWebpageContent.Loaded(loadedContent)
|
||||||
|
|
||||||
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)
|
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling)
|
item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling)
|
||||||
@ -665,7 +670,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let peer = peer {
|
if let peer = peer {
|
||||||
let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL)
|
let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: .channel(peer), safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL)
|
||||||
items.append(item)
|
items.append(item)
|
||||||
if offset.isZero {
|
if offset.isZero {
|
||||||
contentSize.height += 40.0
|
contentSize.height += 40.0
|
||||||
@ -679,10 +684,10 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||||
var items: [InstantPageItem] = []
|
var items: [InstantPageItem] = []
|
||||||
|
|
||||||
if let file = media[audioId] as? TelegramMediaFile {
|
if case let .file(file) = media[audioId] {
|
||||||
let mediaIndex = mediaIndexCounter
|
let mediaIndex = mediaIndexCounter
|
||||||
mediaIndexCounter += 1
|
mediaIndexCounter += 1
|
||||||
let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: nil, credit: nil), webpage: webpage)
|
let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: nil, credit: nil), webpage: webpage)
|
||||||
|
|
||||||
contentSize.height += item.frame.height
|
contentSize.height += item.frame.height
|
||||||
items.append(item)
|
items.append(item)
|
||||||
@ -765,7 +770,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
for (i, article) in articles.enumerated() {
|
for (i, article) in articles.enumerated() {
|
||||||
var cover: TelegramMediaImage?
|
var cover: TelegramMediaImage?
|
||||||
if let coverId = article.photoId {
|
if let coverId = article.photoId {
|
||||||
cover = media[coverId] as? TelegramMediaImage
|
if case let .image(image) = media[coverId] {
|
||||||
|
cover = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var styleStack = InstantPageTextStyleStack()
|
var styleStack = InstantPageTextStyleStack()
|
||||||
@ -820,7 +827,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
|||||||
|
|
||||||
var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0)
|
var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0)
|
||||||
var items: [InstantPageItem] = []
|
var items: [InstantPageItem] = []
|
||||||
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: -1, media: map, url: nil, caption: caption.text, credit: caption.credit), attributes: attributes, interactive: true, roundCorners: false, fit: false)
|
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: -1, media: .geo(map), url: nil, caption: caption.text, credit: caption.credit), attributes: attributes, interactive: true, roundCorners: false, fit: false)
|
||||||
|
|
||||||
items.append(mediaItem)
|
items.append(mediaItem)
|
||||||
contentSize.height += filledSize.height
|
contentSize.height += filledSize.height
|
||||||
@ -850,12 +857,12 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLoc
|
|||||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||||
var items: [InstantPageItem] = []
|
var items: [InstantPageItem] = []
|
||||||
|
|
||||||
var media = instantPage.media
|
var media = instantPage.media.mapValues(EngineMedia.init)
|
||||||
if let image = loadedContent.image, let id = image.id {
|
if let image = loadedContent.image, let id = image.id {
|
||||||
media[id] = image
|
media[id] = .image(image)
|
||||||
}
|
}
|
||||||
if let video = loadedContent.file, let id = video.id {
|
if let video = loadedContent.file, let id = video.id {
|
||||||
media[id] = video
|
media[id] = .file(video)
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaIndexCounter: Int = 0
|
var mediaIndexCounter: Int = 0
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
public struct InstantPageMedia: Equatable {
|
public struct InstantPageMedia: Equatable {
|
||||||
public let index: Int
|
public let index: Int
|
||||||
public let media: Media
|
public let media: EngineMedia
|
||||||
public let url: InstantPageUrlItem?
|
public let url: InstantPageUrlItem?
|
||||||
public let caption: RichText?
|
public let caption: RichText?
|
||||||
public let credit: RichText?
|
public let credit: RichText?
|
||||||
|
|
||||||
public init(index: Int, media: Media, url: InstantPageUrlItem?, caption: RichText?, credit: RichText?) {
|
public init(index: Int, media: EngineMedia, url: InstantPageUrlItem?, caption: RichText?, credit: RichText?) {
|
||||||
self.index = index
|
self.index = index
|
||||||
self.media = media
|
self.media = media
|
||||||
self.url = url
|
self.url = url
|
||||||
@ -18,6 +17,6 @@ public struct InstantPageMedia: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: InstantPageMedia, rhs: InstantPageMedia) -> Bool {
|
public static func ==(lhs: InstantPageMedia, rhs: InstantPageMedia) -> Bool {
|
||||||
return lhs.index == rhs.index && lhs.media.isEqual(to: rhs.media) && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit
|
return lhs.index == rhs.index && lhs.media == rhs.media && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
import AccountContext
|
import AccountContext
|
||||||
@ -22,7 +21,11 @@ struct InstantPageMediaPlaylistItemId: SharedMediaPlaylistItemId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func extractFileMedia(_ item: InstantPageMedia) -> TelegramMediaFile? {
|
private func extractFileMedia(_ item: InstantPageMedia) -> TelegramMediaFile? {
|
||||||
return item.media as? TelegramMediaFile
|
if case let .file(file) = item.media {
|
||||||
|
return file
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
|
final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
|
||||||
@ -114,7 +117,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
|
struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
|
||||||
let webpageId: MediaId
|
let webpageId: EngineMedia.Id
|
||||||
|
|
||||||
func isEqual(to: SharedMediaPlaylistId) -> Bool {
|
func isEqual(to: SharedMediaPlaylistId) -> Bool {
|
||||||
if let to = to as? InstantPageMediaPlaylistId {
|
if let to = to as? InstantPageMediaPlaylistId {
|
||||||
@ -125,7 +128,7 @@ struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation {
|
struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation {
|
||||||
let webpageId: MediaId
|
let webpageId: EngineMedia.Id
|
||||||
|
|
||||||
func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
|
func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
|
||||||
guard let to = to as? InstantPagePlaylistLocation else {
|
guard let to = to as? InstantPagePlaylistLocation else {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -14,12 +13,12 @@ public final class InstantPagePeerReferenceItem: InstantPageItem {
|
|||||||
public let separatesTiles: Bool = false
|
public let separatesTiles: Bool = false
|
||||||
public let medias: [InstantPageMedia] = []
|
public let medias: [InstantPageMedia] = []
|
||||||
|
|
||||||
let initialPeer: Peer
|
let initialPeer: EnginePeer
|
||||||
let safeInset: CGFloat
|
let safeInset: CGFloat
|
||||||
let transparent: Bool
|
let transparent: Bool
|
||||||
let rtl: Bool
|
let rtl: Bool
|
||||||
|
|
||||||
init(frame: CGRect, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool) {
|
init(frame: CGRect, initialPeer: EnginePeer, safeInset: CGFloat, transparent: Bool, rtl: Bool) {
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
self.initialPeer = initialPeer
|
self.initialPeer = initialPeer
|
||||||
self.safeInset = safeInset
|
self.safeInset = safeInset
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
@ -64,13 +63,13 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
|||||||
private let activityIndicator: ActivityIndicator
|
private let activityIndicator: ActivityIndicator
|
||||||
private let checkNode: ASImageNode
|
private let checkNode: ASImageNode
|
||||||
|
|
||||||
var peer: Peer?
|
var peer: EnginePeer?
|
||||||
private var peerDisposable: Disposable?
|
private var peerDisposable: Disposable?
|
||||||
|
|
||||||
private let joinDisposable = MetaDisposable()
|
private let joinDisposable = MetaDisposable()
|
||||||
private var joinState: JoinState = .none
|
private var joinState: JoinState = .none
|
||||||
|
|
||||||
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (EnginePeer) -> Void) {
|
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, initialPeer: EnginePeer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (EnginePeer) -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.nameDisplayOrder = nameDisplayOrder
|
self.nameDisplayOrder = nameDisplayOrder
|
||||||
@ -147,26 +146,26 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
|||||||
|
|
||||||
let account = self.context.account
|
let account = self.context.account
|
||||||
let context = self.context
|
let context = self.context
|
||||||
let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer)
|
let signal: Signal<EnginePeer, NoError> = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer._asPeer())
|
||||||
|> mapToSignal({ peer -> Signal<Peer, NoError> in
|
|> mapToSignal({ peer -> Signal<EnginePeer, NoError> in
|
||||||
if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil {
|
if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil {
|
||||||
return .single(peer) |> then(context.engine.peers.resolvePeerByName(name: username)
|
return .single(.channel(peer)) |> then(context.engine.peers.resolvePeerByName(name: username)
|
||||||
|> mapToSignal({ updatedPeer -> Signal<Peer, NoError> in
|
|> mapToSignal({ updatedPeer -> Signal<EnginePeer, NoError> in
|
||||||
if let updatedPeer = updatedPeer {
|
if let updatedPeer = updatedPeer {
|
||||||
return .single(updatedPeer._asPeer())
|
return .single(updatedPeer)
|
||||||
} else {
|
} else {
|
||||||
return .single(peer)
|
return .single(.channel(peer))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
return .single(peer)
|
return .single(EnginePeer(peer))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in
|
self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.peer = peer
|
strongSelf.peer = peer
|
||||||
if let peer = peer as? TelegramChannel {
|
if case let .channel(peer) = peer {
|
||||||
var joinState = strongSelf.joinState
|
var joinState = strongSelf.joinState
|
||||||
if case .member = peer.participationStatus {
|
if case .member = peer.participationStatus {
|
||||||
switch joinState {
|
switch joinState {
|
||||||
@ -210,7 +209,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
|||||||
private func applyThemeAndStrings(themeUpdated: Bool) {
|
private func applyThemeAndStrings(themeUpdated: Bool) {
|
||||||
if let peer = self.peer {
|
if let peer = self.peer {
|
||||||
let textColor = self.transparent ? UIColor.white : self.theme.panelPrimaryColor
|
let textColor = self.transparent ? UIColor.white : self.theme.panelPrimaryColor
|
||||||
self.nameNode.attributedText = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: textColor)
|
self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: textColor)
|
||||||
}
|
}
|
||||||
let accentColor = self.transparent ? UIColor.white : self.theme.panelAccentColor
|
let accentColor = self.transparent ? UIColor.white : self.theme.panelAccentColor
|
||||||
self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: accentColor), for: [])
|
self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: accentColor), for: [])
|
||||||
@ -300,7 +299,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
|||||||
|
|
||||||
@objc func buttonPressed() {
|
@objc func buttonPressed() {
|
||||||
if let peer = self.peer {
|
if let peer = self.peer {
|
||||||
self.openPeer(EnginePeer(peer))
|
self.openPeer(peer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -29,7 +28,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
|||||||
|
|
||||||
private var currentSize: CGSize?
|
private var currentSize: CGSize?
|
||||||
|
|
||||||
private var fetchStatus: MediaResourceStatus?
|
private var fetchStatus: EngineMediaResource.FetchStatus?
|
||||||
private var fetchedDisposable = MetaDisposable()
|
private var fetchedDisposable = MetaDisposable()
|
||||||
private var statusDisposable = MetaDisposable()
|
private var statusDisposable = MetaDisposable()
|
||||||
|
|
||||||
@ -47,17 +46,19 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
|||||||
self.openMedia = openMedia
|
self.openMedia = openMedia
|
||||||
|
|
||||||
var imageReference: ImageMediaReference?
|
var imageReference: ImageMediaReference?
|
||||||
if let file = media.media as? TelegramMediaFile, let presentation = smallestImageRepresentation(file.previewRepresentations) {
|
if case let .file(file) = media.media, let presentation = smallestImageRepresentation(file.previewRepresentations) {
|
||||||
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
let image = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||||
imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
|
||||||
}
|
}
|
||||||
|
|
||||||
var streamVideo = false
|
var streamVideo = false
|
||||||
if let file = media.media as? TelegramMediaFile {
|
var fileValue: TelegramMediaFile?
|
||||||
|
if case let .file(file) = media.media {
|
||||||
streamVideo = isMediaStreamable(media: file)
|
streamVideo = isMediaStreamable(media: file)
|
||||||
|
fileValue = file
|
||||||
}
|
}
|
||||||
|
|
||||||
self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true)
|
self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true)
|
||||||
self.videoNode.isUserInteractionEnabled = false
|
self.videoNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
|
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
|
||||||
@ -66,13 +67,13 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
|||||||
|
|
||||||
self.addSubnode(self.videoNode)
|
self.addSubnode(self.videoNode)
|
||||||
|
|
||||||
if let file = media.media as? TelegramMediaFile {
|
if case let .file(file) = media.media {
|
||||||
self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start())
|
self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start())
|
||||||
|
|
||||||
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
|
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||||
displayLinkDispatcher.dispatch {
|
displayLinkDispatcher.dispatch {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.fetchStatus = status
|
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
|
||||||
strongSelf.updateFetchStatus()
|
strongSelf.updateFetchStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import AccountContext
|
import AccountContext
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
@ -197,9 +196,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
|||||||
if self.contentNode == nil || self.contentNode?.frame.width != width {
|
if self.contentNode == nil || self.contentNode?.frame.width != width {
|
||||||
self.contentNode?.removeFromSupernode()
|
self.contentNode?.removeFromSupernode()
|
||||||
|
|
||||||
var media: [MediaId: Media] = [:]
|
var media: [EngineMedia.Id: EngineMedia] = [:]
|
||||||
if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage {
|
if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage {
|
||||||
media = instantPage.media
|
media = instantPage.media.mapValues(EngineMedia.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
let sideInset: CGFloat = 16.0
|
let sideInset: CGFloat = 16.0
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Postbox
|
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -186,9 +186,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe
|
|||||||
private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode {
|
private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode {
|
||||||
let media = self.items[index]
|
let media = self.items[index]
|
||||||
let contentNode: ASDisplayNode
|
let contentNode: ASDisplayNode
|
||||||
if let _ = media.media as? TelegramMediaImage {
|
if case .image = media.media {
|
||||||
contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished)
|
contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished)
|
||||||
} else if let _ = media.media as? TelegramMediaFile {
|
} else if case .file = media.media {
|
||||||
contentNode = ASDisplayNode()
|
contentNode = ASDisplayNode()
|
||||||
} else {
|
} else {
|
||||||
contentNode = ASDisplayNode()
|
contentNode = ASDisplayNode()
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ public final class InstantPageStoredState: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal<InstantPageStoredState?, NoError> {
|
public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal<InstantPageStoredState?, NoError> {
|
||||||
let key = ValueBoxKey(length: 8)
|
let key = EngineDataBuffer(length: 8)
|
||||||
key.setInt64(0, value: webPage.webpageId.id)
|
key.setInt64(0, value: webPage.webpageId.id)
|
||||||
|
|
||||||
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, id: key))
|
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, id: key))
|
||||||
@ -68,7 +67,7 @@ public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMedi
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func updateInstantPageStoredStateInteractively(engine: TelegramEngine, webPage: TelegramMediaWebpage, state: InstantPageStoredState?) -> Signal<Never, NoError> {
|
public func updateInstantPageStoredStateInteractively(engine: TelegramEngine, webPage: TelegramMediaWebpage, state: InstantPageStoredState?) -> Signal<Never, NoError> {
|
||||||
let key = ValueBoxKey(length: 8)
|
let key = EngineDataBuffer(length: 8)
|
||||||
key.setInt64(0, value: webPage.webpageId.id)
|
key.setInt64(0, value: webPage.webpageId.id)
|
||||||
|
|
||||||
if let state = state {
|
if let state = state {
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
|
||||||
import Display
|
import Display
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
@ -277,7 +276,7 @@ private func offestForVerticalAlignment(_ verticalAlignment: TableVerticalAlignm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: InstantPageTextStyleStack, theme: InstantPageTheme, bordered: Bool, striped: Bool, boundingWidth: CGFloat, horizontalInset: CGFloat, media: [MediaId: Media], webpage: TelegramMediaWebpage) -> InstantPageTableItem {
|
func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: InstantPageTextStyleStack, theme: InstantPageTheme, bordered: Bool, striped: Bool, boundingWidth: CGFloat, horizontalInset: CGFloat, media: [EngineMedia.Id: EngineMedia], webpage: TelegramMediaWebpage) -> InstantPageTableItem {
|
||||||
if rows.count == 0 {
|
if rows.count == 0 {
|
||||||
return InstantPageTableItem(frame: CGRect(), totalWidth: 0.0, horizontalInset: 0.0, borderWidth: 0.0, theme: theme, cells: [], rtl: rtl)
|
return InstantPageTableItem(frame: CGRect(), totalWidth: 0.0, horizontalInset: 0.0, borderWidth: 0.0, theme: theme, cells: [], rtl: rtl)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Display
|
import Display
|
||||||
import Postbox
|
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TelegramUIPreferences
|
import TelegramUIPreferences
|
||||||
@ -12,9 +11,9 @@ import ContextUI
|
|||||||
|
|
||||||
public final class InstantPageUrlItem: Equatable {
|
public final class InstantPageUrlItem: Equatable {
|
||||||
public let url: String
|
public let url: String
|
||||||
public let webpageId: MediaId?
|
public let webpageId: EngineMedia.Id?
|
||||||
|
|
||||||
public init(url: String, webpageId: MediaId?) {
|
public init(url: String, webpageId: EngineMedia.Id?) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.webpageId = webpageId
|
self.webpageId = webpageId
|
||||||
}
|
}
|
||||||
@ -36,7 +35,7 @@ struct InstantPageTextStrikethroughItem {
|
|||||||
struct InstantPageTextImageItem {
|
struct InstantPageTextImageItem {
|
||||||
let frame: CGRect
|
let frame: CGRect
|
||||||
let range: NSRange
|
let range: NSRange
|
||||||
let id: MediaId
|
let id: EngineMedia.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InstantPageTextAnchorItem {
|
struct InstantPageTextAnchorItem {
|
||||||
@ -649,7 +648,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [MediaId: Media] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) {
|
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [EngineMedia.Id: EngineMedia] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) {
|
||||||
if string.length == 0 {
|
if string.length == 0 {
|
||||||
return (nil, [], CGSize())
|
return (nil, [], CGSize())
|
||||||
}
|
}
|
||||||
@ -771,7 +770,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
|
|||||||
extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
|
extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
|
||||||
}
|
}
|
||||||
maxImageHeight = max(maxImageHeight, imageFrame.height)
|
maxImageHeight = max(maxImageHeight, imageFrame.height)
|
||||||
lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: MediaId(namespace: Namespaces.Media.CloudFile, id: id)))
|
lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: id)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -876,8 +875,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
|
|||||||
for line in textItem.lines {
|
for line in textItem.lines {
|
||||||
let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
|
let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
|
||||||
for imageItem in line.imageItems {
|
for imageItem in line.imageItems {
|
||||||
if let image = media[imageItem.id] as? TelegramMediaFile {
|
if case let .image(image) = media[imageItem.id] {
|
||||||
let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: image, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
|
let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: .image(image), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
|
||||||
additionalItems.append(item)
|
additionalItems.append(item)
|
||||||
|
|
||||||
if item.frame.minY < topInset {
|
if item.frame.minY < topInset {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Postbox
|
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
@ -389,7 +389,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
|
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
|
||||||
avatarListWrapperNode.contentNode.clipsToBounds = true
|
avatarListWrapperNode.contentNode.clipsToBounds = true
|
||||||
avatarListNode.backgroundColor = .clear
|
avatarListNode.backgroundColor = .clear
|
||||||
avatarListNode.peer = peer
|
avatarListNode.peer = EnginePeer(peer)
|
||||||
avatarListNode.firstFullSizeOnly = true
|
avatarListNode.firstFullSizeOnly = true
|
||||||
avatarListNode.offsetLocation = true
|
avatarListNode.offsetLocation = true
|
||||||
avatarListNode.customCenterTapAction = { [weak self] in
|
avatarListNode.customCenterTapAction = { [weak self] in
|
||||||
@ -405,7 +405,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
|
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
|
||||||
avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode)
|
avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode)
|
||||||
|
|
||||||
avatarListNode.update(size: targetRect.size, peer: peer, customNode: nil, additionalEntry: .single(nil), isExpanded: true, transition: .immediate)
|
avatarListNode.update(size: targetRect.size, peer: EnginePeer(peer), customNode: nil, additionalEntry: .single(nil), isExpanded: true, transition: .immediate)
|
||||||
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
|
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
|
||||||
|
|
||||||
strongSelf.avatarListWrapperNode = avatarListWrapperNode
|
strongSelf.avatarListWrapperNode = avatarListWrapperNode
|
||||||
|
@ -878,7 +878,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
|
|
||||||
if statusUpdated && item.displayFileInfo {
|
if statusUpdated && item.displayFileInfo {
|
||||||
if let file = selectedMedia as? TelegramMediaFile {
|
if let file = selectedMedia as? TelegramMediaFile {
|
||||||
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|
||||||
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
||||||
if case .Fetching = value.fetchStatus, !item.isDownloadList {
|
if case .Fetching = value.fetchStatus, !item.isDownloadList {
|
||||||
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||||
@ -905,10 +905,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isVoice {
|
if isVoice {
|
||||||
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|
||||||
}
|
}
|
||||||
} else if let image = selectedMedia as? TelegramMediaImage {
|
} else if let image = selectedMedia as? TelegramMediaImage {
|
||||||
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|
||||||
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
|
||||||
if case .Fetching = value.fetchStatus, !item.isDownloadList {
|
if case .Fetching = value.fetchStatus, !item.isDownloadList {
|
||||||
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
|
||||||
load(
|
|
||||||
"@build_bazel_rules_apple//apple:resources.bzl",
|
|
||||||
"apple_resource_bundle",
|
|
||||||
"apple_resource_group",
|
|
||||||
)
|
|
||||||
load("//build-system/bazel-utils:plist_fragment.bzl",
|
|
||||||
"plist_fragment",
|
|
||||||
)
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "LottieMeshSwiftMetalResources",
|
|
||||||
srcs = glob([
|
|
||||||
"Resources/**/*.metal",
|
|
||||||
]),
|
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
)
|
|
||||||
|
|
||||||
plist_fragment(
|
|
||||||
name = "LottieMeshSwiftBundleInfoPlist",
|
|
||||||
extension = "plist",
|
|
||||||
template =
|
|
||||||
"""
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>org.telegram.LottieMeshSwift</string>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>en</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>LottieMeshSwift</string>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
apple_resource_bundle(
|
|
||||||
name = "LottieMeshSwiftBundle",
|
|
||||||
infoplists = [
|
|
||||||
":LottieMeshSwiftBundleInfoPlist",
|
|
||||||
],
|
|
||||||
resources = [
|
|
||||||
":LottieMeshSwiftMetalResources",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
config_setting(
|
|
||||||
name = "debug_build",
|
|
||||||
values = {
|
|
||||||
"compilation_mode": "dbg",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
optimization_flags = select({
|
|
||||||
":debug_build": [
|
|
||||||
"-O2",
|
|
||||||
],
|
|
||||||
"//conditions:default": [],
|
|
||||||
})
|
|
||||||
|
|
||||||
swift_optimization_flags = select({
|
|
||||||
":debug_build": [
|
|
||||||
#"-O",
|
|
||||||
],
|
|
||||||
"//conditions:default": [],
|
|
||||||
})
|
|
||||||
|
|
||||||
swift_library(
|
|
||||||
name = "LottieMeshSwift",
|
|
||||||
module_name = "LottieMeshSwift",
|
|
||||||
srcs = glob([
|
|
||||||
"Sources/**/*.swift",
|
|
||||||
]),
|
|
||||||
copts = [
|
|
||||||
"-warnings-as-errors",
|
|
||||||
] + swift_optimization_flags,
|
|
||||||
data = [
|
|
||||||
":LottieMeshSwiftBundle",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":LottieMeshBinding",
|
|
||||||
"//submodules/Postbox:Postbox",
|
|
||||||
"//submodules/ManagedFile:ManagedFile",
|
|
||||||
],
|
|
||||||
visibility = [
|
|
||||||
"//visibility:public",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
objc_library(
|
|
||||||
name = "LottieMeshBinding",
|
|
||||||
enable_modules = True,
|
|
||||||
module_name = "LottieMeshBinding",
|
|
||||||
srcs = glob([
|
|
||||||
"LottieMeshBinding/Sources/**/*.m",
|
|
||||||
"LottieMeshBinding/Sources/**/*.mm",
|
|
||||||
"LottieMeshBinding/Sources/**/*.h",
|
|
||||||
]),
|
|
||||||
copts = optimization_flags,
|
|
||||||
hdrs = glob([
|
|
||||||
"LottieMeshBinding/PublicHeaders/**/*.h",
|
|
||||||
]),
|
|
||||||
includes = [
|
|
||||||
"LottieMeshBinding/PublicHeaders",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":LottieMesh",
|
|
||||||
],
|
|
||||||
sdk_frameworks = [
|
|
||||||
"Foundation",
|
|
||||||
],
|
|
||||||
visibility = [
|
|
||||||
"//visibility:public",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
cc_library(
|
|
||||||
name = "LottieMesh",
|
|
||||||
srcs = glob([
|
|
||||||
"LottieMesh/Sources/**/*.cpp",
|
|
||||||
"LottieMesh/Sources/**/*.h",
|
|
||||||
"LottieMesh/Sources/**/*.hpp",
|
|
||||||
]),
|
|
||||||
copts = [
|
|
||||||
"-Isubmodules/LottieMeshSwift/libtess2/Include",
|
|
||||||
] + optimization_flags,
|
|
||||||
hdrs = glob([
|
|
||||||
"LottieMesh/PublicHeaders/**/*.h",
|
|
||||||
"LottieMesh/PublicHeaders/**/*.hpp",
|
|
||||||
]),
|
|
||||||
includes = [
|
|
||||||
"LottieMesh/PublicHeaders",
|
|
||||||
],
|
|
||||||
deps = [
|
|
||||||
":libtess2",
|
|
||||||
],
|
|
||||||
visibility = [
|
|
||||||
"//visibility:public",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
cc_library(
|
|
||||||
name = "libtess2",
|
|
||||||
srcs = glob([
|
|
||||||
"libtess2/Sources/**/*.c",
|
|
||||||
"libtess2/Sources/**/*.h",
|
|
||||||
"libtess2/Include/**/*.h",
|
|
||||||
]),
|
|
||||||
copts = [
|
|
||||||
"-Isubmodules/LottieMeshSwift/libtess2/Include",
|
|
||||||
] + optimization_flags,
|
|
||||||
hdrs = glob([
|
|
||||||
"libtess2/Include/**/*.h",
|
|
||||||
]),
|
|
||||||
deps = [
|
|
||||||
],
|
|
||||||
visibility = [
|
|
||||||
"//visibility:public",
|
|
||||||
],
|
|
||||||
)
|
|
@ -1,60 +0,0 @@
|
|||||||
#ifndef LottieMesh_h
|
|
||||||
#define LottieMesh_h
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Point.h"
|
|
||||||
|
|
||||||
namespace MeshGenerator {
|
|
||||||
|
|
||||||
struct Path {
|
|
||||||
std::vector<Point> points;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Fill {
|
|
||||||
enum class Rule {
|
|
||||||
EvenOdd,
|
|
||||||
NonZero
|
|
||||||
};
|
|
||||||
|
|
||||||
Rule rule = Rule::EvenOdd;
|
|
||||||
|
|
||||||
explicit Fill(Rule rule_) :
|
|
||||||
rule(rule_) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Stroke {
|
|
||||||
enum class LineJoin {
|
|
||||||
Miter,
|
|
||||||
Round,
|
|
||||||
Bevel
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class LineCap {
|
|
||||||
Butt,
|
|
||||||
Round,
|
|
||||||
Square
|
|
||||||
};
|
|
||||||
|
|
||||||
float lineWidth = 0.0f;
|
|
||||||
LineJoin lineJoin = LineJoin::Round;
|
|
||||||
LineCap lineCap = LineCap::Round;
|
|
||||||
float miterLimit = 10.0f;
|
|
||||||
|
|
||||||
explicit Stroke(float lineWidth_, LineJoin lineJoin_, LineCap lineCap_, float miterLimit_) :
|
|
||||||
lineWidth(lineWidth_), lineJoin(lineJoin_), lineCap(lineCap_), miterLimit(miterLimit_) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Mesh {
|
|
||||||
std::vector<Point> vertices;
|
|
||||||
std::vector<int> triangles;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::unique_ptr<Mesh> generateMesh(std::vector<Path> const &paths, std::unique_ptr<Fill> fill, std::unique_ptr<Stroke> stroke);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif /* LottieMesh_h */
|
|
@ -1,47 +0,0 @@
|
|||||||
#ifndef Point_h
|
|
||||||
#define Point_h
|
|
||||||
|
|
||||||
#include <math.h>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace MeshGenerator {
|
|
||||||
|
|
||||||
struct Point {
|
|
||||||
float x = 0.0f;
|
|
||||||
float y = 0.0f;
|
|
||||||
|
|
||||||
Point(float x_, float y_) :
|
|
||||||
x(x_), y(y_) {
|
|
||||||
}
|
|
||||||
|
|
||||||
Point() : Point(0.0f, 0.0f) {
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isEqual(Point const &other, float epsilon = 0.0001f) const {
|
|
||||||
return std::abs(x - other.x) <= epsilon && std::abs(y - other.y) <= epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
float distance(Point const &other) const {
|
|
||||||
float dx = x - other.x;
|
|
||||||
float dy = y - other.y;
|
|
||||||
return sqrtf(dx * dx + dy * dy);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator< (Point const &other) const {
|
|
||||||
if (x < other.x) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (x > other.x) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return y < other.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator== (Point const &other) const {
|
|
||||||
return isEqual(other);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,86 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Vec2.h"
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
namespace crushedpixel {
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
struct LineSegment {
|
|
||||||
LineSegment(const Vec2 &a, const Vec2 &b) :
|
|
||||||
a(a), b(b) {}
|
|
||||||
|
|
||||||
Vec2 a, b;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return A copy of the line segment, offset by the given vector.
|
|
||||||
*/
|
|
||||||
LineSegment operator+(const Vec2 &toAdd) const {
|
|
||||||
return {Vec2Maths::add(a, toAdd), Vec2Maths::add(b, toAdd)};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return A copy of the line segment, offset by the given vector.
|
|
||||||
*/
|
|
||||||
LineSegment operator-(const Vec2 &toRemove) const {
|
|
||||||
return {Vec2Maths::subtract(a, toRemove), Vec2Maths::subtract(b, toRemove)};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The line segment's normal vector.
|
|
||||||
*/
|
|
||||||
Vec2 normal() const {
|
|
||||||
auto dir = direction();
|
|
||||||
|
|
||||||
// return the direction vector
|
|
||||||
// rotated by 90 degrees counter-clockwise
|
|
||||||
return {-dir.y, dir.x};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The line segment's direction vector.
|
|
||||||
*/
|
|
||||||
Vec2 direction(bool normalized = true) const {
|
|
||||||
auto vec = Vec2Maths::subtract(b, a);
|
|
||||||
|
|
||||||
return normalized
|
|
||||||
? Vec2Maths::normalized(vec)
|
|
||||||
: vec;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Vec2 intersection(const LineSegment &a, const LineSegment &b, bool infiniteLines, bool &success) {
|
|
||||||
success = true;
|
|
||||||
|
|
||||||
// calculate un-normalized direction vectors
|
|
||||||
auto r = a.direction(false);
|
|
||||||
auto s = b.direction(false);
|
|
||||||
|
|
||||||
auto originDist = Vec2Maths::subtract(b.a, a.a);
|
|
||||||
|
|
||||||
auto uNumerator = Vec2Maths::cross(originDist, r);
|
|
||||||
auto denominator = Vec2Maths::cross(r, s);
|
|
||||||
|
|
||||||
if (std::abs(denominator) < 0.0001f) {
|
|
||||||
// The lines are parallel
|
|
||||||
success = false;
|
|
||||||
return Vec2();
|
|
||||||
}
|
|
||||||
|
|
||||||
// solve the intersection positions
|
|
||||||
auto u = uNumerator / denominator;
|
|
||||||
auto t = Vec2Maths::cross(originDist, s) / denominator;
|
|
||||||
|
|
||||||
if (!infiniteLines && (t < 0 || t > 1 || u < 0 || u > 1)) {
|
|
||||||
// the intersection lies outside of the line segments
|
|
||||||
success = false;
|
|
||||||
return Vec2();
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate the intersection point
|
|
||||||
// a.a + r * t;
|
|
||||||
return Vec2Maths::add(a.a, Vec2Maths::multiply(r, t));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace crushedpixel
|
|
@ -1,95 +0,0 @@
|
|||||||
#include <LottieMesh/LottieMesh.h>
|
|
||||||
|
|
||||||
#include <LottieMesh/Point.h>
|
|
||||||
#include "Triangulation.h"
|
|
||||||
|
|
||||||
#include "tesselator.h"
|
|
||||||
#include "Polyline2D.h"
|
|
||||||
|
|
||||||
namespace MeshGenerator {
|
|
||||||
|
|
||||||
std::unique_ptr<Mesh> generateMesh(std::vector<Path> const &paths, std::unique_ptr<Fill> fill, std::unique_ptr<Stroke> stroke) {
|
|
||||||
if (stroke) {
|
|
||||||
std::unique_ptr<Mesh> mesh = std::make_unique<Mesh>();
|
|
||||||
|
|
||||||
for (const auto &path : paths) {
|
|
||||||
crushedpixel::Polyline2D::JointStyle jointStyle = crushedpixel::Polyline2D::JointStyle::ROUND;
|
|
||||||
crushedpixel::Polyline2D::EndCapStyle endCapStyle = crushedpixel::Polyline2D::EndCapStyle::SQUARE;
|
|
||||||
switch (stroke->lineJoin) {
|
|
||||||
case Stroke::LineJoin::Miter:
|
|
||||||
jointStyle = crushedpixel::Polyline2D::JointStyle::MITER;
|
|
||||||
break;
|
|
||||||
case Stroke::LineJoin::Round:
|
|
||||||
jointStyle = crushedpixel::Polyline2D::JointStyle::ROUND;
|
|
||||||
break;
|
|
||||||
case Stroke::LineJoin::Bevel:
|
|
||||||
jointStyle = crushedpixel::Polyline2D::JointStyle::BEVEL;
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (stroke->lineCap) {
|
|
||||||
case Stroke::LineCap::Round: {
|
|
||||||
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::ROUND;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Stroke::LineCap::Square: {
|
|
||||||
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::SQUARE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Stroke::LineCap::Butt: {
|
|
||||||
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::BUTT;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto vertices = crushedpixel::Polyline2D::create(path.points, stroke->lineWidth, jointStyle, endCapStyle);
|
|
||||||
for (const auto &vertex : vertices) {
|
|
||||||
mesh->triangles.push_back((int)mesh->vertices.size());
|
|
||||||
mesh->vertices.push_back(vertex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(mesh->triangles.size() % 3 == 0);
|
|
||||||
return mesh;
|
|
||||||
} else if (fill) {
|
|
||||||
TESStesselator *tessellator = tessNewTess(NULL);
|
|
||||||
tessSetOption(tessellator, TESS_CONSTRAINED_DELAUNAY_TRIANGULATION, 1);
|
|
||||||
for (const auto &path : paths) {
|
|
||||||
tessAddContour(tessellator, 2, path.points.data(), sizeof(Point), (int)path.points.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (fill->rule) {
|
|
||||||
case Fill::Rule::EvenOdd: {
|
|
||||||
tessTesselate(tessellator, TESS_WINDING_ODD, TESS_POLYGONS, 3, 2, NULL);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
tessTesselate(tessellator, TESS_WINDING_NONZERO, TESS_POLYGONS, 3, 2, NULL);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int vertexCount = tessGetVertexCount(tessellator);
|
|
||||||
const TESSreal *vertices = tessGetVertices(tessellator);
|
|
||||||
int indexCount = tessGetElementCount(tessellator) * 3;
|
|
||||||
const TESSindex *indices = tessGetElements(tessellator);
|
|
||||||
|
|
||||||
std::unique_ptr<Mesh> mesh = std::make_unique<Mesh>();
|
|
||||||
for (int i = 0; i < vertexCount; i++) {
|
|
||||||
mesh->vertices.push_back(Point(vertices[i * 2 + 0], vertices[i * 2 + 1]));
|
|
||||||
}
|
|
||||||
for (int i = 0; i < indexCount; i++) {
|
|
||||||
mesh->triangles.push_back(indices[i]);
|
|
||||||
}
|
|
||||||
return mesh;
|
|
||||||
} else {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,446 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "LineSegment.h"
|
|
||||||
#include <vector>
|
|
||||||
#include <iterator>
|
|
||||||
#include <cassert>
|
|
||||||
|
|
||||||
namespace crushedpixel {
|
|
||||||
|
|
||||||
class Polyline2D {
|
|
||||||
public:
|
|
||||||
enum class JointStyle {
|
|
||||||
/**
|
|
||||||
* Corners are drawn with sharp joints.
|
|
||||||
* If the joint's outer angle is too large,
|
|
||||||
* the joint is drawn as beveled instead,
|
|
||||||
* to avoid the miter extending too far out.
|
|
||||||
*/
|
|
||||||
MITER,
|
|
||||||
/**
|
|
||||||
* Corners are flattened.
|
|
||||||
*/
|
|
||||||
BEVEL,
|
|
||||||
/**
|
|
||||||
* Corners are rounded off.
|
|
||||||
*/
|
|
||||||
ROUND
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class EndCapStyle {
|
|
||||||
/**
|
|
||||||
* Path ends are drawn flat,
|
|
||||||
* and don't exceed the actual end point.
|
|
||||||
*/
|
|
||||||
BUTT, // lol
|
|
||||||
/**
|
|
||||||
* Path ends are drawn flat,
|
|
||||||
* but extended beyond the end point
|
|
||||||
* by half the line thickness.
|
|
||||||
*/
|
|
||||||
SQUARE,
|
|
||||||
/**
|
|
||||||
* Path ends are rounded off.
|
|
||||||
*/
|
|
||||||
ROUND,
|
|
||||||
/**
|
|
||||||
* Path ends are connected according to the JointStyle.
|
|
||||||
* When using this EndCapStyle, don't specify the common start/end point twice,
|
|
||||||
* as Polyline2D connects the first and last input point itself.
|
|
||||||
*/
|
|
||||||
JOINT
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a vector of vertices describing a solid path through the input points.
|
|
||||||
* @param points The points of the path.
|
|
||||||
* @param thickness The path's thickness.
|
|
||||||
* @param jointStyle The path's joint style.
|
|
||||||
* @param endCapStyle The path's end cap style.
|
|
||||||
* @param allowOverlap Whether to allow overlapping vertices.
|
|
||||||
* This yields better results when dealing with paths
|
|
||||||
* whose points have a distance smaller than the thickness,
|
|
||||||
* but may introduce overlapping vertices,
|
|
||||||
* which is undesirable when rendering transparent paths.
|
|
||||||
* @return The vertices describing the path.
|
|
||||||
* @tparam Vec2 The vector type to use for the vertices.
|
|
||||||
* Must have public non-const float fields "x" and "y".
|
|
||||||
* Must have a two-args constructor taking x and y values.
|
|
||||||
* See crushedpixel::Vec2 for a type that satisfies these requirements.
|
|
||||||
* @tparam InputCollection The collection type of the input points.
|
|
||||||
* Must contain elements of type Vec2.
|
|
||||||
* Must expose size() and operator[] functions.
|
|
||||||
*/
|
|
||||||
template<typename Vec2, typename InputCollection>
|
|
||||||
static std::vector<Vec2> create(const InputCollection &points, float thickness,
|
|
||||||
JointStyle jointStyle = JointStyle::MITER,
|
|
||||||
EndCapStyle endCapStyle = EndCapStyle::BUTT,
|
|
||||||
bool allowOverlap = false) {
|
|
||||||
std::vector<Vec2> vertices;
|
|
||||||
create(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap);
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static std::vector<Vec2> create(const std::vector<Vec2> &points, float thickness,
|
|
||||||
JointStyle jointStyle = JointStyle::MITER,
|
|
||||||
EndCapStyle endCapStyle = EndCapStyle::BUTT,
|
|
||||||
bool allowOverlap = false) {
|
|
||||||
std::vector<Vec2> vertices;
|
|
||||||
create<Vec2, std::vector<Vec2>>(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap);
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2, typename InputCollection>
|
|
||||||
static size_t create(std::vector<Vec2> &vertices, const InputCollection &points, float thickness,
|
|
||||||
JointStyle jointStyle = JointStyle::MITER,
|
|
||||||
EndCapStyle endCapStyle = EndCapStyle::BUTT,
|
|
||||||
bool allowOverlap = false) {
|
|
||||||
auto numVerticesBefore = vertices.size();
|
|
||||||
|
|
||||||
create<Vec2, InputCollection>(std::back_inserter(vertices), points, thickness,
|
|
||||||
jointStyle, endCapStyle, allowOverlap);
|
|
||||||
|
|
||||||
return vertices.size() - numVerticesBefore;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2, typename InputCollection, typename OutputIterator>
|
|
||||||
static OutputIterator create(OutputIterator vertices, const InputCollection &points, float thickness,
|
|
||||||
JointStyle jointStyle = JointStyle::MITER,
|
|
||||||
EndCapStyle endCapStyle = EndCapStyle::BUTT,
|
|
||||||
bool allowOverlap = false) {
|
|
||||||
// operate on half the thickness to make our lives easier
|
|
||||||
thickness /= 2;
|
|
||||||
|
|
||||||
// create poly segments from the points
|
|
||||||
std::vector<PolySegment<Vec2>> segments;
|
|
||||||
for (size_t i = 0; i + 1 < points.size(); i++) {
|
|
||||||
auto &point1 = points[i];
|
|
||||||
auto &point2 = points[i + 1];
|
|
||||||
|
|
||||||
// to avoid division-by-zero errors,
|
|
||||||
// only create a line segment for non-identical points
|
|
||||||
if (!Vec2Maths::equal(point1, point2)) {
|
|
||||||
segments.emplace_back(LineSegment<Vec2>(point1, point2), thickness);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endCapStyle == EndCapStyle::JOINT) {
|
|
||||||
// create a connecting segment from the last to the first point
|
|
||||||
|
|
||||||
auto &point1 = points[points.size() - 1];
|
|
||||||
auto &point2 = points[0];
|
|
||||||
|
|
||||||
// to avoid division-by-zero errors,
|
|
||||||
// only create a line segment for non-identical points
|
|
||||||
if (!Vec2Maths::equal(point1, point2)) {
|
|
||||||
segments.emplace_back(LineSegment<Vec2>(point1, point2), thickness);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segments.empty()) {
|
|
||||||
// handle the case of insufficient input points
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec2 nextStart1{0, 0};
|
|
||||||
Vec2 nextStart2{0, 0};
|
|
||||||
Vec2 start1{0, 0};
|
|
||||||
Vec2 start2{0, 0};
|
|
||||||
Vec2 end1{0, 0};
|
|
||||||
Vec2 end2{0, 0};
|
|
||||||
|
|
||||||
// calculate the path's global start and end points
|
|
||||||
auto &firstSegment = segments[0];
|
|
||||||
auto &lastSegment = segments[segments.size() - 1];
|
|
||||||
|
|
||||||
auto pathStart1 = firstSegment.edge1.a;
|
|
||||||
auto pathStart2 = firstSegment.edge2.a;
|
|
||||||
auto pathEnd1 = lastSegment.edge1.b;
|
|
||||||
auto pathEnd2 = lastSegment.edge2.b;
|
|
||||||
|
|
||||||
// handle different end cap styles
|
|
||||||
if (endCapStyle == EndCapStyle::SQUARE) {
|
|
||||||
// extend the start/end points by half the thickness
|
|
||||||
pathStart1 = Vec2Maths::subtract(pathStart1, Vec2Maths::multiply(firstSegment.edge1.direction(), thickness));
|
|
||||||
pathStart2 = Vec2Maths::subtract(pathStart2, Vec2Maths::multiply(firstSegment.edge2.direction(), thickness));
|
|
||||||
pathEnd1 = Vec2Maths::add(pathEnd1, Vec2Maths::multiply(lastSegment.edge1.direction(), thickness));
|
|
||||||
pathEnd2 = Vec2Maths::add(pathEnd2, Vec2Maths::multiply(lastSegment.edge2.direction(), thickness));
|
|
||||||
|
|
||||||
} else if (endCapStyle == EndCapStyle::ROUND) {
|
|
||||||
// draw half circle end caps
|
|
||||||
createTriangleFan(vertices, firstSegment.center.a, firstSegment.center.a,
|
|
||||||
firstSegment.edge1.a, firstSegment.edge2.a, false);
|
|
||||||
createTriangleFan(vertices, lastSegment.center.b, lastSegment.center.b,
|
|
||||||
lastSegment.edge1.b, lastSegment.edge2.b, true);
|
|
||||||
|
|
||||||
} else if (endCapStyle == EndCapStyle::JOINT) {
|
|
||||||
// join the last (connecting) segment and the first segment
|
|
||||||
createJoint(vertices, lastSegment, firstSegment, jointStyle,
|
|
||||||
pathEnd1, pathEnd2, pathStart1, pathStart2, allowOverlap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate mesh data for path segments
|
|
||||||
for (size_t i = 0; i < segments.size(); i++) {
|
|
||||||
auto &segment = segments[i];
|
|
||||||
|
|
||||||
// calculate start
|
|
||||||
if (i == 0) {
|
|
||||||
// this is the first segment
|
|
||||||
start1 = pathStart1;
|
|
||||||
start2 = pathStart2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i + 1 == segments.size()) {
|
|
||||||
// this is the last segment
|
|
||||||
end1 = pathEnd1;
|
|
||||||
end2 = pathEnd2;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
createJoint(vertices, segment, segments[i + 1], jointStyle,
|
|
||||||
end1, end2, nextStart1, nextStart2, allowOverlap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit vertices
|
|
||||||
*vertices++ = start1;
|
|
||||||
*vertices++ = start2;
|
|
||||||
*vertices++ = end1;
|
|
||||||
|
|
||||||
*vertices++ = end1;
|
|
||||||
*vertices++ = start2;
|
|
||||||
*vertices++ = end2;
|
|
||||||
|
|
||||||
start1 = nextStart1;
|
|
||||||
start2 = nextStart2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
static constexpr float pi = 3.14159265358979323846f;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The threshold for mitered joints.
|
|
||||||
* If the joint's angle is smaller than this angle,
|
|
||||||
* the joint will be drawn beveled instead.
|
|
||||||
*/
|
|
||||||
static constexpr float miterMinAngle = 0.349066; // ~20 degrees
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The minimum angle of a round joint's triangles.
|
|
||||||
*/
|
|
||||||
static constexpr float roundMinAngle = 0.174533; // ~10 degrees
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
struct PolySegment {
|
|
||||||
PolySegment(const LineSegment<Vec2> ¢er, float thickness) :
|
|
||||||
center(center),
|
|
||||||
// calculate the segment's outer edges by offsetting
|
|
||||||
// the central line by the normal vector
|
|
||||||
// multiplied with the thickness
|
|
||||||
|
|
||||||
// center + center.normal() * thickness
|
|
||||||
edge1(center + Vec2Maths::multiply(center.normal(), thickness)),
|
|
||||||
edge2(center - Vec2Maths::multiply(center.normal(), thickness)) {}
|
|
||||||
|
|
||||||
LineSegment<Vec2> center, edge1, edge2;
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename Vec2, typename OutputIterator>
|
|
||||||
static OutputIterator createJoint(OutputIterator vertices,
|
|
||||||
const PolySegment<Vec2> &segment1, const PolySegment<Vec2> &segment2,
|
|
||||||
JointStyle jointStyle, Vec2 &end1, Vec2 &end2,
|
|
||||||
Vec2 &nextStart1, Vec2 &nextStart2,
|
|
||||||
bool allowOverlap) {
|
|
||||||
// calculate the angle between the two line segments
|
|
||||||
auto dir1 = segment1.center.direction();
|
|
||||||
auto dir2 = segment2.center.direction();
|
|
||||||
|
|
||||||
auto angle = Vec2Maths::angle(dir1, dir2);
|
|
||||||
|
|
||||||
// wrap the angle around the 180° mark if it exceeds 90°
|
|
||||||
// for minimum angle detection
|
|
||||||
auto wrappedAngle = angle;
|
|
||||||
if (wrappedAngle > pi / 2) {
|
|
||||||
wrappedAngle = pi - wrappedAngle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jointStyle == JointStyle::MITER && wrappedAngle < miterMinAngle) {
|
|
||||||
// the minimum angle for mitered joints wasn't exceeded.
|
|
||||||
// to avoid the intersection point being extremely far out,
|
|
||||||
// thus producing an enormous joint like a rasta on 4/20,
|
|
||||||
// we render the joint beveled instead.
|
|
||||||
jointStyle = JointStyle::BEVEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (jointStyle == JointStyle::MITER) {
|
|
||||||
// calculate each edge's intersection point
|
|
||||||
// with the next segment's central line
|
|
||||||
bool sec1Success = true;
|
|
||||||
bool sec2Success = true;
|
|
||||||
auto sec1 = LineSegment<Vec2>::intersection(segment1.edge1, segment2.edge1, true, sec1Success);
|
|
||||||
auto sec2 = LineSegment<Vec2>::intersection(segment1.edge2, segment2.edge2, true, sec2Success);
|
|
||||||
|
|
||||||
end1 = sec1Success ? sec1 : segment1.edge1.b;
|
|
||||||
end2 = sec2Success ? sec2 : segment1.edge2.b;
|
|
||||||
|
|
||||||
nextStart1 = end1;
|
|
||||||
nextStart2 = end2;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// joint style is either BEVEL or ROUND
|
|
||||||
|
|
||||||
// find out which are the inner edges for this joint
|
|
||||||
auto x1 = dir1.x;
|
|
||||||
auto x2 = dir2.x;
|
|
||||||
auto y1 = dir1.y;
|
|
||||||
auto y2 = dir2.y;
|
|
||||||
|
|
||||||
auto clockwise = x1 * y2 - x2 * y1 < 0;
|
|
||||||
|
|
||||||
const LineSegment<Vec2> *inner1, *inner2, *outer1, *outer2;
|
|
||||||
|
|
||||||
// as the normal vector is rotated counter-clockwise,
|
|
||||||
// the first edge lies to the left
|
|
||||||
// from the central line's perspective,
|
|
||||||
// and the second one to the right.
|
|
||||||
if (clockwise) {
|
|
||||||
outer1 = &segment1.edge1;
|
|
||||||
outer2 = &segment2.edge1;
|
|
||||||
inner1 = &segment1.edge2;
|
|
||||||
inner2 = &segment2.edge2;
|
|
||||||
} else {
|
|
||||||
outer1 = &segment1.edge2;
|
|
||||||
outer2 = &segment2.edge2;
|
|
||||||
inner1 = &segment1.edge1;
|
|
||||||
inner2 = &segment2.edge1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate the intersection point of the inner edges
|
|
||||||
bool innerSecOptSuccess = true;
|
|
||||||
auto innerSecOpt = LineSegment<Vec2>::intersection(*inner1, *inner2, allowOverlap, innerSecOptSuccess);
|
|
||||||
|
|
||||||
auto innerSec = innerSecOptSuccess
|
|
||||||
? innerSecOpt
|
|
||||||
// for parallel lines, simply connect them directly
|
|
||||||
: inner1->b;
|
|
||||||
|
|
||||||
// if there's no inner intersection, flip
|
|
||||||
// the next start position for near-180° turns
|
|
||||||
Vec2 innerStart;
|
|
||||||
if (innerSecOptSuccess) {
|
|
||||||
innerStart = innerSec;
|
|
||||||
} else if (angle > pi / 2) {
|
|
||||||
innerStart = outer1->b;
|
|
||||||
} else {
|
|
||||||
innerStart = inner1->b;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clockwise) {
|
|
||||||
end1 = outer1->b;
|
|
||||||
end2 = innerSec;
|
|
||||||
|
|
||||||
nextStart1 = outer2->a;
|
|
||||||
nextStart2 = innerStart;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
end1 = innerSec;
|
|
||||||
end2 = outer1->b;
|
|
||||||
|
|
||||||
nextStart1 = innerStart;
|
|
||||||
nextStart2 = outer2->a;
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect the intersection points according to the joint style
|
|
||||||
|
|
||||||
if (jointStyle == JointStyle::BEVEL) {
|
|
||||||
// simply connect the intersection points
|
|
||||||
*vertices++ = outer1->b;
|
|
||||||
*vertices++ = outer2->a;
|
|
||||||
*vertices++ = innerSec;
|
|
||||||
|
|
||||||
} else if (jointStyle == JointStyle::ROUND) {
|
|
||||||
// draw a circle between the ends of the outer edges,
|
|
||||||
// centered at the actual point
|
|
||||||
// with half the line thickness as the radius
|
|
||||||
createTriangleFan(vertices, innerSec, segment1.center.b, outer1->b, outer2->a, clockwise);
|
|
||||||
} else {
|
|
||||||
assert(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a partial circle between two points.
|
|
||||||
* The points must be equally far away from the origin.
|
|
||||||
* @param vertices The vector to add vertices to.
|
|
||||||
* @param connectTo The position to connect the triangles to.
|
|
||||||
* @param origin The circle's origin.
|
|
||||||
* @param start The circle's starting point.
|
|
||||||
* @param end The circle's ending point.
|
|
||||||
* @param clockwise Whether the circle's rotation is clockwise.
|
|
||||||
*/
|
|
||||||
template<typename Vec2, typename OutputIterator>
|
|
||||||
static OutputIterator createTriangleFan(OutputIterator vertices, Vec2 connectTo, Vec2 origin,
|
|
||||||
Vec2 start, Vec2 end, bool clockwise) {
|
|
||||||
|
|
||||||
auto point1 = Vec2Maths::subtract(start, origin);
|
|
||||||
auto point2 = Vec2Maths::subtract(end, origin);
|
|
||||||
|
|
||||||
// calculate the angle between the two points
|
|
||||||
auto angle1 = atan2(point1.y, point1.x);
|
|
||||||
auto angle2 = atan2(point2.y, point2.x);
|
|
||||||
|
|
||||||
// ensure the outer angle is calculated
|
|
||||||
if (clockwise) {
|
|
||||||
if (angle2 > angle1) {
|
|
||||||
angle2 = angle2 - 2 * pi;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (angle1 > angle2) {
|
|
||||||
angle1 = angle1 - 2 * pi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto jointAngle = angle2 - angle1;
|
|
||||||
|
|
||||||
// calculate the amount of triangles to use for the joint
|
|
||||||
auto numTriangles = std::max(1, (int) std::floor(std::abs(jointAngle) / roundMinAngle));
|
|
||||||
|
|
||||||
// calculate the angle of each triangle
|
|
||||||
auto triAngle = jointAngle / numTriangles;
|
|
||||||
|
|
||||||
Vec2 startPoint = start;
|
|
||||||
Vec2 endPoint;
|
|
||||||
for (int t = 0; t < numTriangles; t++) {
|
|
||||||
if (t + 1 == numTriangles) {
|
|
||||||
// it's the last triangle - ensure it perfectly
|
|
||||||
// connects to the next line
|
|
||||||
endPoint = end;
|
|
||||||
} else {
|
|
||||||
auto rot = (t + 1) * triAngle;
|
|
||||||
|
|
||||||
// rotate the original point around the origin
|
|
||||||
endPoint.x = std::cos(rot) * point1.x - std::sin(rot) * point1.y;
|
|
||||||
endPoint.y = std::sin(rot) * point1.x + std::cos(rot) * point1.y;
|
|
||||||
|
|
||||||
// re-add the rotation origin to the target point
|
|
||||||
endPoint = Vec2Maths::add(endPoint, origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// emit the triangle
|
|
||||||
*vertices++ = startPoint;
|
|
||||||
*vertices++ = endPoint;
|
|
||||||
*vertices++ = connectTo;
|
|
||||||
|
|
||||||
startPoint = endPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
return vertices;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace crushedpixel
|
|
@ -1,51 +0,0 @@
|
|||||||
#include "Triangulation.h"
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <array>
|
|
||||||
|
|
||||||
#include "earcut.hpp"
|
|
||||||
|
|
||||||
namespace MeshGenerator {
|
|
||||||
|
|
||||||
std::vector<uint32_t> triangulatePolygon(std::vector<Point> const &points, std::vector<int> &indices, std::vector<std::vector<int>> const &holeIndices) {
|
|
||||||
// The index type. Defaults to uint32_t, but you can also pass uint16_t if you know that your
|
|
||||||
// data won't have more than 65536 vertices.
|
|
||||||
using N = uint32_t;
|
|
||||||
|
|
||||||
// Create array
|
|
||||||
using EarPoint = std::array<float, 2>;
|
|
||||||
std::vector<std::vector<EarPoint>> polygon;
|
|
||||||
|
|
||||||
std::map<int, int> facePointMapping;
|
|
||||||
int nextFacePointIndex = 0;
|
|
||||||
|
|
||||||
std::vector<EarPoint> facePoints;
|
|
||||||
for (auto index : indices) {
|
|
||||||
facePointMapping[nextFacePointIndex] = index;
|
|
||||||
nextFacePointIndex++;
|
|
||||||
|
|
||||||
facePoints.push_back({ points[index].x, points[index].y });
|
|
||||||
}
|
|
||||||
polygon.push_back(std::move(facePoints));
|
|
||||||
|
|
||||||
for (const auto &list : holeIndices) {
|
|
||||||
std::vector<EarPoint> holePoints;
|
|
||||||
for (auto index : list) {
|
|
||||||
facePointMapping[nextFacePointIndex] = index;
|
|
||||||
nextFacePointIndex++;
|
|
||||||
|
|
||||||
holePoints.push_back({ points[index].x, points[index].y });
|
|
||||||
}
|
|
||||||
polygon.push_back(std::move(holePoints));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<N> triangleIndices = mapbox::earcut<N>(polygon);
|
|
||||||
|
|
||||||
std::vector<uint32_t> mappedIndices;
|
|
||||||
for (auto index : triangleIndices) {
|
|
||||||
mappedIndices.push_back(facePointMapping[index]);
|
|
||||||
}
|
|
||||||
return mappedIndices;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
#ifndef Triangulation_h
|
|
||||||
#define Triangulation_h
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <LottieMesh/Point.h>
|
|
||||||
|
|
||||||
namespace MeshGenerator {
|
|
||||||
|
|
||||||
std::vector<uint32_t> triangulatePolygon(std::vector<Point> const &points, std::vector<int> &indices, std::vector<std::vector<int>> const &holeIndices);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif /* Triangulation_h */
|
|
@ -1,99 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace crushedpixel {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A two-dimensional float vector.
|
|
||||||
* It exposes the x and y fields
|
|
||||||
* as required by the Polyline2D functions.
|
|
||||||
*/
|
|
||||||
struct Vec2 {
|
|
||||||
Vec2() :
|
|
||||||
Vec2(0, 0) {}
|
|
||||||
|
|
||||||
Vec2(float x, float y) :
|
|
||||||
x(x), y(y) {}
|
|
||||||
|
|
||||||
virtual ~Vec2() = default;
|
|
||||||
|
|
||||||
float x, y;
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace Vec2Maths {
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static bool equal(const Vec2 &a, const Vec2 &b) {
|
|
||||||
return a.x == b.x && a.y == b.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static Vec2 multiply(const Vec2 &a, const Vec2 &b) {
|
|
||||||
return {a.x * b.x, a.y * b.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static Vec2 multiply(const Vec2 &vec, float factor) {
|
|
||||||
return {vec.x * factor, vec.y * factor};
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static Vec2 divide(const Vec2 &vec, float factor) {
|
|
||||||
return {vec.x / factor, vec.y / factor};
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static Vec2 add(const Vec2 &a, const Vec2 &b) {
|
|
||||||
return {a.x + b.x, a.y + b.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static Vec2 subtract(const Vec2 &a, const Vec2 &b) {
|
|
||||||
return {a.x - b.x, a.y - b.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static float magnitude(const Vec2 &vec) {
|
|
||||||
return std::sqrt(vec.x * vec.x + vec.y * vec.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static Vec2 withLength(const Vec2 &vec, float len) {
|
|
||||||
auto mag = magnitude(vec);
|
|
||||||
auto factor = mag / len;
|
|
||||||
return divide(vec, factor);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Vec2>
|
|
||||||
static Vec2 normalized(const Vec2 &vec) {
|
|
||||||
return withLength(vec, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the dot product of two vectors.
|
|
||||||
*/
|
|
||||||
template<typename Vec2>
|
|
||||||
static float dot(const Vec2 &a, const Vec2 &b) {
|
|
||||||
return a.x * b.x + a.y * b.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the cross product of two vectors.
|
|
||||||
*/
|
|
||||||
template<typename Vec2>
|
|
||||||
static float cross(const Vec2 &a, const Vec2 &b) {
|
|
||||||
return a.x * b.y - a.y * b.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the angle between two vectors.
|
|
||||||
*/
|
|
||||||
template<typename Vec2>
|
|
||||||
static float angle(const Vec2 &a, const Vec2 &b) {
|
|
||||||
return std::acos(dot(a, b) / (magnitude(a) * magnitude(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Vec2Maths
|
|
||||||
|
|
||||||
}
|
|
@ -1,823 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cassert>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <limits>
|
|
||||||
#include <memory>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace mapbox {
|
|
||||||
|
|
||||||
namespace util {
|
|
||||||
|
|
||||||
template <std::size_t I, typename T> struct nth {
|
|
||||||
inline static typename std::tuple_element<I, T>::type
|
|
||||||
get(const T& t) { return std::get<I>(t); };
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace detail {
|
|
||||||
|
|
||||||
template <typename N = uint32_t>
|
|
||||||
class Earcut {
|
|
||||||
public:
|
|
||||||
std::vector<N> indices;
|
|
||||||
std::size_t vertices = 0;
|
|
||||||
|
|
||||||
template <typename Polygon>
|
|
||||||
void operator()(const Polygon& points);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct Node {
|
|
||||||
Node(N index, double x_, double y_) : i(index), x(x_), y(y_) {}
|
|
||||||
Node(const Node&) = delete;
|
|
||||||
Node& operator=(const Node&) = delete;
|
|
||||||
Node(Node&&) = delete;
|
|
||||||
Node& operator=(Node&&) = delete;
|
|
||||||
|
|
||||||
const N i;
|
|
||||||
const double x;
|
|
||||||
const double y;
|
|
||||||
|
|
||||||
// previous and next vertice nodes in a polygon ring
|
|
||||||
Node* prev = nullptr;
|
|
||||||
Node* next = nullptr;
|
|
||||||
|
|
||||||
// z-order curve value
|
|
||||||
int32_t z = 0;
|
|
||||||
|
|
||||||
// previous and next nodes in z-order
|
|
||||||
Node* prevZ = nullptr;
|
|
||||||
Node* nextZ = nullptr;
|
|
||||||
|
|
||||||
// indicates whether this is a steiner point
|
|
||||||
bool steiner = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename Ring> Node* linkedList(const Ring& points, const bool clockwise);
|
|
||||||
Node* filterPoints(Node* start, Node* end = nullptr);
|
|
||||||
void earcutLinked(Node* ear, int pass = 0);
|
|
||||||
bool isEar(Node* ear);
|
|
||||||
bool isEarHashed(Node* ear);
|
|
||||||
Node* cureLocalIntersections(Node* start);
|
|
||||||
void splitEarcut(Node* start);
|
|
||||||
template <typename Polygon> Node* eliminateHoles(const Polygon& points, Node* outerNode);
|
|
||||||
Node* eliminateHole(Node* hole, Node* outerNode);
|
|
||||||
Node* findHoleBridge(Node* hole, Node* outerNode);
|
|
||||||
bool sectorContainsSector(const Node* m, const Node* p);
|
|
||||||
void indexCurve(Node* start);
|
|
||||||
Node* sortLinked(Node* list);
|
|
||||||
int32_t zOrder(const double x_, const double y_);
|
|
||||||
Node* getLeftmost(Node* start);
|
|
||||||
bool pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const;
|
|
||||||
bool isValidDiagonal(Node* a, Node* b);
|
|
||||||
double area(const Node* p, const Node* q, const Node* r) const;
|
|
||||||
bool equals(const Node* p1, const Node* p2);
|
|
||||||
bool intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2);
|
|
||||||
bool onSegment(const Node* p, const Node* q, const Node* r);
|
|
||||||
int sign(double val);
|
|
||||||
bool intersectsPolygon(const Node* a, const Node* b);
|
|
||||||
bool locallyInside(const Node* a, const Node* b);
|
|
||||||
bool middleInside(const Node* a, const Node* b);
|
|
||||||
Node* splitPolygon(Node* a, Node* b);
|
|
||||||
template <typename Point> Node* insertNode(std::size_t i, const Point& p, Node* last);
|
|
||||||
void removeNode(Node* p);
|
|
||||||
|
|
||||||
bool hashing;
|
|
||||||
double minX, maxX;
|
|
||||||
double minY, maxY;
|
|
||||||
double inv_size = 0;
|
|
||||||
|
|
||||||
template <typename T, typename Alloc = std::allocator<T>>
|
|
||||||
class ObjectPool {
|
|
||||||
public:
|
|
||||||
ObjectPool() { }
|
|
||||||
ObjectPool(std::size_t blockSize_) {
|
|
||||||
reset(blockSize_);
|
|
||||||
}
|
|
||||||
~ObjectPool() {
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
template <typename... Args>
|
|
||||||
T* construct(Args&&... args) {
|
|
||||||
if (currentIndex >= blockSize) {
|
|
||||||
currentBlock = alloc_traits::allocate(alloc, blockSize);
|
|
||||||
allocations.emplace_back(currentBlock);
|
|
||||||
currentIndex = 0;
|
|
||||||
}
|
|
||||||
T* object = ¤tBlock[currentIndex++];
|
|
||||||
alloc_traits::construct(alloc, object, std::forward<Args>(args)...);
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
void reset(std::size_t newBlockSize) {
|
|
||||||
for (auto allocation : allocations) {
|
|
||||||
alloc_traits::deallocate(alloc, allocation, blockSize);
|
|
||||||
}
|
|
||||||
allocations.clear();
|
|
||||||
blockSize = std::max<std::size_t>(1, newBlockSize);
|
|
||||||
currentBlock = nullptr;
|
|
||||||
currentIndex = blockSize;
|
|
||||||
}
|
|
||||||
void clear() { reset(blockSize); }
|
|
||||||
private:
|
|
||||||
T* currentBlock = nullptr;
|
|
||||||
std::size_t currentIndex = 1;
|
|
||||||
std::size_t blockSize = 1;
|
|
||||||
std::vector<T*> allocations;
|
|
||||||
Alloc alloc;
|
|
||||||
typedef typename std::allocator_traits<Alloc> alloc_traits;
|
|
||||||
};
|
|
||||||
ObjectPool<Node> nodes;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename N> template <typename Polygon>
|
|
||||||
void Earcut<N>::operator()(const Polygon& points) {
|
|
||||||
// reset
|
|
||||||
indices.clear();
|
|
||||||
vertices = 0;
|
|
||||||
|
|
||||||
if (points.empty()) return;
|
|
||||||
|
|
||||||
double x;
|
|
||||||
double y;
|
|
||||||
int threshold = 80;
|
|
||||||
std::size_t len = 0;
|
|
||||||
|
|
||||||
for (size_t i = 0; threshold >= 0 && i < points.size(); i++) {
|
|
||||||
threshold -= static_cast<int>(points[i].size());
|
|
||||||
len += points[i].size();
|
|
||||||
}
|
|
||||||
|
|
||||||
//estimate size of nodes and indices
|
|
||||||
nodes.reset(len * 3 / 2);
|
|
||||||
indices.reserve(len + points[0].size());
|
|
||||||
|
|
||||||
Node* outerNode = linkedList(points[0], true);
|
|
||||||
if (!outerNode || outerNode->prev == outerNode->next) return;
|
|
||||||
|
|
||||||
if (points.size() > 1) outerNode = eliminateHoles(points, outerNode);
|
|
||||||
|
|
||||||
// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
|
|
||||||
hashing = threshold < 0;
|
|
||||||
if (hashing) {
|
|
||||||
Node* p = outerNode->next;
|
|
||||||
minX = maxX = outerNode->x;
|
|
||||||
minY = maxY = outerNode->y;
|
|
||||||
do {
|
|
||||||
x = p->x;
|
|
||||||
y = p->y;
|
|
||||||
minX = std::min<double>(minX, x);
|
|
||||||
minY = std::min<double>(minY, y);
|
|
||||||
maxX = std::max<double>(maxX, x);
|
|
||||||
maxY = std::max<double>(maxY, y);
|
|
||||||
p = p->next;
|
|
||||||
} while (p != outerNode);
|
|
||||||
|
|
||||||
// minX, minY and size are later used to transform coords into integers for z-order calculation
|
|
||||||
inv_size = std::max<double>(maxX - minX, maxY - minY);
|
|
||||||
inv_size = inv_size != .0 ? (1. / inv_size) : .0;
|
|
||||||
}
|
|
||||||
|
|
||||||
earcutLinked(outerNode);
|
|
||||||
|
|
||||||
nodes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a circular doubly linked list from polygon points in the specified winding order
|
|
||||||
template <typename N> template <typename Ring>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::linkedList(const Ring& points, const bool clockwise) {
|
|
||||||
using Point = typename Ring::value_type;
|
|
||||||
double sum = 0;
|
|
||||||
const std::size_t len = points.size();
|
|
||||||
std::size_t i, j;
|
|
||||||
Node* last = nullptr;
|
|
||||||
|
|
||||||
// calculate original winding order of a polygon ring
|
|
||||||
for (i = 0, j = len > 0 ? len - 1 : 0; i < len; j = i++) {
|
|
||||||
const auto& p1 = points[i];
|
|
||||||
const auto& p2 = points[j];
|
|
||||||
const double p20 = util::nth<0, Point>::get(p2);
|
|
||||||
const double p10 = util::nth<0, Point>::get(p1);
|
|
||||||
const double p11 = util::nth<1, Point>::get(p1);
|
|
||||||
const double p21 = util::nth<1, Point>::get(p2);
|
|
||||||
sum += (p20 - p10) * (p11 + p21);
|
|
||||||
}
|
|
||||||
|
|
||||||
// link points into circular doubly-linked list in the specified winding order
|
|
||||||
if (clockwise == (sum > 0)) {
|
|
||||||
for (i = 0; i < len; i++) last = insertNode(vertices + i, points[i], last);
|
|
||||||
} else {
|
|
||||||
for (i = len; i-- > 0;) last = insertNode(vertices + i, points[i], last);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (last && equals(last, last->next)) {
|
|
||||||
removeNode(last);
|
|
||||||
last = last->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
vertices += len;
|
|
||||||
|
|
||||||
return last;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eliminate colinear or duplicate points
|
|
||||||
template <typename N>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::filterPoints(Node* start, Node* end) {
|
|
||||||
if (!end) end = start;
|
|
||||||
|
|
||||||
Node* p = start;
|
|
||||||
bool again;
|
|
||||||
do {
|
|
||||||
again = false;
|
|
||||||
|
|
||||||
if (!p->steiner && (equals(p, p->next) || area(p->prev, p, p->next) == 0)) {
|
|
||||||
removeNode(p);
|
|
||||||
p = end = p->prev;
|
|
||||||
|
|
||||||
if (p == p->next) break;
|
|
||||||
again = true;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
p = p->next;
|
|
||||||
}
|
|
||||||
} while (again || p != end);
|
|
||||||
|
|
||||||
return end;
|
|
||||||
}
|
|
||||||
|
|
||||||
// main ear slicing loop which triangulates a polygon (given as a linked list)
|
|
||||||
template <typename N>
|
|
||||||
void Earcut<N>::earcutLinked(Node* ear, int pass) {
|
|
||||||
if (!ear) return;
|
|
||||||
|
|
||||||
// interlink polygon nodes in z-order
|
|
||||||
if (!pass && hashing) indexCurve(ear);
|
|
||||||
|
|
||||||
Node* stop = ear;
|
|
||||||
Node* prev;
|
|
||||||
Node* next;
|
|
||||||
|
|
||||||
int iterations = 0;
|
|
||||||
|
|
||||||
// iterate through ears, slicing them one by one
|
|
||||||
while (ear->prev != ear->next) {
|
|
||||||
iterations++;
|
|
||||||
prev = ear->prev;
|
|
||||||
next = ear->next;
|
|
||||||
|
|
||||||
if (hashing ? isEarHashed(ear) : isEar(ear)) {
|
|
||||||
// cut off the triangle
|
|
||||||
indices.emplace_back(prev->i);
|
|
||||||
indices.emplace_back(ear->i);
|
|
||||||
indices.emplace_back(next->i);
|
|
||||||
|
|
||||||
removeNode(ear);
|
|
||||||
|
|
||||||
// skipping the next vertice leads to less sliver triangles
|
|
||||||
ear = next->next;
|
|
||||||
stop = next->next;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ear = next;
|
|
||||||
|
|
||||||
// if we looped through the whole remaining polygon and can't find any more ears
|
|
||||||
if (ear == stop) {
|
|
||||||
// try filtering points and slicing again
|
|
||||||
if (!pass) earcutLinked(filterPoints(ear), 1);
|
|
||||||
|
|
||||||
// if this didn't work, try curing all small self-intersections locally
|
|
||||||
else if (pass == 1) {
|
|
||||||
ear = cureLocalIntersections(filterPoints(ear));
|
|
||||||
earcutLinked(ear, 2);
|
|
||||||
|
|
||||||
// as a last resort, try splitting the remaining polygon into two
|
|
||||||
} else if (pass == 2) splitEarcut(ear);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check whether a polygon node forms a valid ear with adjacent nodes
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::isEar(Node* ear) {
|
|
||||||
const Node* a = ear->prev;
|
|
||||||
const Node* b = ear;
|
|
||||||
const Node* c = ear->next;
|
|
||||||
|
|
||||||
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
|
|
||||||
|
|
||||||
// now make sure we don't have other points inside the potential ear
|
|
||||||
Node* p = ear->next->next;
|
|
||||||
|
|
||||||
while (p != ear->prev) {
|
|
||||||
if (pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
|
|
||||||
area(p->prev, p, p->next) >= 0) return false;
|
|
||||||
p = p->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::isEarHashed(Node* ear) {
|
|
||||||
const Node* a = ear->prev;
|
|
||||||
const Node* b = ear;
|
|
||||||
const Node* c = ear->next;
|
|
||||||
|
|
||||||
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
|
|
||||||
|
|
||||||
// triangle bbox; min & max are calculated like this for speed
|
|
||||||
const double minTX = std::min<double>(a->x, std::min<double>(b->x, c->x));
|
|
||||||
const double minTY = std::min<double>(a->y, std::min<double>(b->y, c->y));
|
|
||||||
const double maxTX = std::max<double>(a->x, std::max<double>(b->x, c->x));
|
|
||||||
const double maxTY = std::max<double>(a->y, std::max<double>(b->y, c->y));
|
|
||||||
|
|
||||||
// z-order range for the current triangle bbox;
|
|
||||||
const int32_t minZ = zOrder(minTX, minTY);
|
|
||||||
const int32_t maxZ = zOrder(maxTX, maxTY);
|
|
||||||
|
|
||||||
// first look for points inside the triangle in increasing z-order
|
|
||||||
Node* p = ear->nextZ;
|
|
||||||
|
|
||||||
while (p && p->z <= maxZ) {
|
|
||||||
if (p != ear->prev && p != ear->next &&
|
|
||||||
pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
|
|
||||||
area(p->prev, p, p->next) >= 0) return false;
|
|
||||||
p = p->nextZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
// then look for points in decreasing z-order
|
|
||||||
p = ear->prevZ;
|
|
||||||
|
|
||||||
while (p && p->z >= minZ) {
|
|
||||||
if (p != ear->prev && p != ear->next &&
|
|
||||||
pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
|
|
||||||
area(p->prev, p, p->next) >= 0) return false;
|
|
||||||
p = p->prevZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// go through all polygon nodes and cure small local self-intersections
|
|
||||||
template <typename N>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::cureLocalIntersections(Node* start) {
|
|
||||||
Node* p = start;
|
|
||||||
do {
|
|
||||||
Node* a = p->prev;
|
|
||||||
Node* b = p->next->next;
|
|
||||||
|
|
||||||
// a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2])
|
|
||||||
if (!equals(a, b) && intersects(a, p, p->next, b) && locallyInside(a, b) && locallyInside(b, a)) {
|
|
||||||
indices.emplace_back(a->i);
|
|
||||||
indices.emplace_back(p->i);
|
|
||||||
indices.emplace_back(b->i);
|
|
||||||
|
|
||||||
// remove two nodes involved
|
|
||||||
removeNode(p);
|
|
||||||
removeNode(p->next);
|
|
||||||
|
|
||||||
p = start = b;
|
|
||||||
}
|
|
||||||
p = p->next;
|
|
||||||
} while (p != start);
|
|
||||||
|
|
||||||
return filterPoints(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
// try splitting polygon into two and triangulate them independently
|
|
||||||
template <typename N>
|
|
||||||
void Earcut<N>::splitEarcut(Node* start) {
|
|
||||||
// look for a valid diagonal that divides the polygon into two
|
|
||||||
Node* a = start;
|
|
||||||
do {
|
|
||||||
Node* b = a->next->next;
|
|
||||||
while (b != a->prev) {
|
|
||||||
if (a->i != b->i && isValidDiagonal(a, b)) {
|
|
||||||
// split the polygon in two by the diagonal
|
|
||||||
Node* c = splitPolygon(a, b);
|
|
||||||
|
|
||||||
// filter colinear points around the cuts
|
|
||||||
a = filterPoints(a, a->next);
|
|
||||||
c = filterPoints(c, c->next);
|
|
||||||
|
|
||||||
// run earcut on each half
|
|
||||||
earcutLinked(a);
|
|
||||||
earcutLinked(c);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
b = b->next;
|
|
||||||
}
|
|
||||||
a = a->next;
|
|
||||||
} while (a != start);
|
|
||||||
}
|
|
||||||
|
|
||||||
// link every hole into the outer loop, producing a single-ring polygon without holes
|
|
||||||
template <typename N> template <typename Polygon>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::eliminateHoles(const Polygon& points, Node* outerNode) {
|
|
||||||
const size_t len = points.size();
|
|
||||||
|
|
||||||
std::vector<Node*> queue;
|
|
||||||
for (size_t i = 1; i < len; i++) {
|
|
||||||
Node* list = linkedList(points[i], false);
|
|
||||||
if (list) {
|
|
||||||
if (list == list->next) list->steiner = true;
|
|
||||||
queue.push_back(getLeftmost(list));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::sort(queue.begin(), queue.end(), [](const Node* a, const Node* b) {
|
|
||||||
return a->x < b->x;
|
|
||||||
});
|
|
||||||
|
|
||||||
// process holes from left to right
|
|
||||||
for (size_t i = 0; i < queue.size(); i++) {
|
|
||||||
outerNode = eliminateHole(queue[i], outerNode);
|
|
||||||
outerNode = filterPoints(outerNode, outerNode->next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return outerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// find a bridge between vertices that connects hole with an outer ring and and link it
|
|
||||||
template <typename N>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::eliminateHole(Node* hole, Node* outerNode) {
|
|
||||||
Node* bridge = findHoleBridge(hole, outerNode);
|
|
||||||
if (!bridge) {
|
|
||||||
return outerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
Node* bridgeReverse = splitPolygon(bridge, hole);
|
|
||||||
|
|
||||||
// filter collinear points around the cuts
|
|
||||||
Node* filteredBridge = filterPoints(bridge, bridge->next);
|
|
||||||
filterPoints(bridgeReverse, bridgeReverse->next);
|
|
||||||
|
|
||||||
// Check if input node was removed by the filtering
|
|
||||||
return outerNode == bridge ? filteredBridge : outerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// David Eberly's algorithm for finding a bridge between hole and outer polygon
|
|
||||||
template <typename N>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::findHoleBridge(Node* hole, Node* outerNode) {
|
|
||||||
Node* p = outerNode;
|
|
||||||
double hx = hole->x;
|
|
||||||
double hy = hole->y;
|
|
||||||
double qx = -std::numeric_limits<double>::infinity();
|
|
||||||
Node* m = nullptr;
|
|
||||||
|
|
||||||
// find a segment intersected by a ray from the hole's leftmost Vertex to the left;
|
|
||||||
// segment's endpoint with lesser x will be potential connection Vertex
|
|
||||||
do {
|
|
||||||
if (hy <= p->y && hy >= p->next->y && p->next->y != p->y) {
|
|
||||||
double x = p->x + (hy - p->y) * (p->next->x - p->x) / (p->next->y - p->y);
|
|
||||||
if (x <= hx && x > qx) {
|
|
||||||
qx = x;
|
|
||||||
if (x == hx) {
|
|
||||||
if (hy == p->y) return p;
|
|
||||||
if (hy == p->next->y) return p->next;
|
|
||||||
}
|
|
||||||
m = p->x < p->next->x ? p : p->next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p = p->next;
|
|
||||||
} while (p != outerNode);
|
|
||||||
|
|
||||||
if (!m) return 0;
|
|
||||||
|
|
||||||
if (hx == qx) return m; // hole touches outer segment; pick leftmost endpoint
|
|
||||||
|
|
||||||
// look for points inside the triangle of hole Vertex, segment intersection and endpoint;
|
|
||||||
// if there are no points found, we have a valid connection;
|
|
||||||
// otherwise choose the Vertex of the minimum angle with the ray as connection Vertex
|
|
||||||
|
|
||||||
const Node* stop = m;
|
|
||||||
double tanMin = std::numeric_limits<double>::infinity();
|
|
||||||
double tanCur = 0;
|
|
||||||
|
|
||||||
p = m;
|
|
||||||
double mx = m->x;
|
|
||||||
double my = m->y;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if (hx >= p->x && p->x >= mx && hx != p->x &&
|
|
||||||
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p->x, p->y)) {
|
|
||||||
|
|
||||||
tanCur = std::abs(hy - p->y) / (hx - p->x); // tangential
|
|
||||||
|
|
||||||
if (locallyInside(p, hole) &&
|
|
||||||
(tanCur < tanMin || (tanCur == tanMin && (p->x > m->x || sectorContainsSector(m, p))))) {
|
|
||||||
m = p;
|
|
||||||
tanMin = tanCur;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p = p->next;
|
|
||||||
} while (p != stop);
|
|
||||||
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// whether sector in vertex m contains sector in vertex p in the same coordinates
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::sectorContainsSector(const Node* m, const Node* p) {
|
|
||||||
return area(m->prev, m, p->prev) < 0 && area(p->next, m, m->next) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// interlink polygon nodes in z-order
|
|
||||||
template <typename N>
|
|
||||||
void Earcut<N>::indexCurve(Node* start) {
|
|
||||||
assert(start);
|
|
||||||
Node* p = start;
|
|
||||||
|
|
||||||
do {
|
|
||||||
p->z = p->z ? p->z : zOrder(p->x, p->y);
|
|
||||||
p->prevZ = p->prev;
|
|
||||||
p->nextZ = p->next;
|
|
||||||
p = p->next;
|
|
||||||
} while (p != start);
|
|
||||||
|
|
||||||
p->prevZ->nextZ = nullptr;
|
|
||||||
p->prevZ = nullptr;
|
|
||||||
|
|
||||||
sortLinked(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simon Tatham's linked list merge sort algorithm
|
|
||||||
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
|
|
||||||
template <typename N>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::sortLinked(Node* list) {
|
|
||||||
assert(list);
|
|
||||||
Node* p;
|
|
||||||
Node* q;
|
|
||||||
Node* e;
|
|
||||||
Node* tail;
|
|
||||||
int i, numMerges, pSize, qSize;
|
|
||||||
int inSize = 1;
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
p = list;
|
|
||||||
list = nullptr;
|
|
||||||
tail = nullptr;
|
|
||||||
numMerges = 0;
|
|
||||||
|
|
||||||
while (p) {
|
|
||||||
numMerges++;
|
|
||||||
q = p;
|
|
||||||
pSize = 0;
|
|
||||||
for (i = 0; i < inSize; i++) {
|
|
||||||
pSize++;
|
|
||||||
q = q->nextZ;
|
|
||||||
if (!q) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
qSize = inSize;
|
|
||||||
|
|
||||||
while (pSize > 0 || (qSize > 0 && q)) {
|
|
||||||
|
|
||||||
if (pSize == 0) {
|
|
||||||
e = q;
|
|
||||||
q = q->nextZ;
|
|
||||||
qSize--;
|
|
||||||
} else if (qSize == 0 || !q) {
|
|
||||||
e = p;
|
|
||||||
p = p->nextZ;
|
|
||||||
pSize--;
|
|
||||||
} else if (p->z <= q->z) {
|
|
||||||
e = p;
|
|
||||||
p = p->nextZ;
|
|
||||||
pSize--;
|
|
||||||
} else {
|
|
||||||
e = q;
|
|
||||||
q = q->nextZ;
|
|
||||||
qSize--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tail) tail->nextZ = e;
|
|
||||||
else list = e;
|
|
||||||
|
|
||||||
e->prevZ = tail;
|
|
||||||
tail = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
p = q;
|
|
||||||
}
|
|
||||||
|
|
||||||
tail->nextZ = nullptr;
|
|
||||||
|
|
||||||
if (numMerges <= 1) return list;
|
|
||||||
|
|
||||||
inSize *= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// z-order of a Vertex given coords and size of the data bounding box
|
|
||||||
template <typename N>
|
|
||||||
int32_t Earcut<N>::zOrder(const double x_, const double y_) {
|
|
||||||
// coords are transformed into non-negative 15-bit integer range
|
|
||||||
int32_t x = static_cast<int32_t>(32767.0 * (x_ - minX) * inv_size);
|
|
||||||
int32_t y = static_cast<int32_t>(32767.0 * (y_ - minY) * inv_size);
|
|
||||||
|
|
||||||
x = (x | (x << 8)) & 0x00FF00FF;
|
|
||||||
x = (x | (x << 4)) & 0x0F0F0F0F;
|
|
||||||
x = (x | (x << 2)) & 0x33333333;
|
|
||||||
x = (x | (x << 1)) & 0x55555555;
|
|
||||||
|
|
||||||
y = (y | (y << 8)) & 0x00FF00FF;
|
|
||||||
y = (y | (y << 4)) & 0x0F0F0F0F;
|
|
||||||
y = (y | (y << 2)) & 0x33333333;
|
|
||||||
y = (y | (y << 1)) & 0x55555555;
|
|
||||||
|
|
||||||
return x | (y << 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the leftmost node of a polygon ring
|
|
||||||
template <typename N>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::getLeftmost(Node* start) {
|
|
||||||
Node* p = start;
|
|
||||||
Node* leftmost = start;
|
|
||||||
do {
|
|
||||||
if (p->x < leftmost->x || (p->x == leftmost->x && p->y < leftmost->y))
|
|
||||||
leftmost = p;
|
|
||||||
p = p->next;
|
|
||||||
} while (p != start);
|
|
||||||
|
|
||||||
return leftmost;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a point lies within a convex triangle
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const {
|
|
||||||
return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
|
|
||||||
(ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
|
|
||||||
(bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::isValidDiagonal(Node* a, Node* b) {
|
|
||||||
return a->next->i != b->i && a->prev->i != b->i && !intersectsPolygon(a, b) && // dones't intersect other edges
|
|
||||||
((locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
|
|
||||||
(area(a->prev, a, b->prev) != 0.0 || area(a, b->prev, b) != 0.0)) || // does not create opposite-facing sectors
|
|
||||||
(equals(a, b) && area(a->prev, a, a->next) > 0 && area(b->prev, b, b->next) > 0)); // special zero-length case
|
|
||||||
}
|
|
||||||
|
|
||||||
// signed area of a triangle
|
|
||||||
template <typename N>
|
|
||||||
double Earcut<N>::area(const Node* p, const Node* q, const Node* r) const {
|
|
||||||
return (q->y - p->y) * (r->x - q->x) - (q->x - p->x) * (r->y - q->y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if two points are equal
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::equals(const Node* p1, const Node* p2) {
|
|
||||||
return p1->x == p2->x && p1->y == p2->y;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if two segments intersect
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2) {
|
|
||||||
int o1 = sign(area(p1, q1, p2));
|
|
||||||
int o2 = sign(area(p1, q1, q2));
|
|
||||||
int o3 = sign(area(p2, q2, p1));
|
|
||||||
int o4 = sign(area(p2, q2, q1));
|
|
||||||
|
|
||||||
if (o1 != o2 && o3 != o4) return true; // general case
|
|
||||||
|
|
||||||
if (o1 == 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
|
|
||||||
if (o2 == 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
|
|
||||||
if (o3 == 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
|
|
||||||
if (o4 == 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// for collinear points p, q, r, check if point q lies on segment pr
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::onSegment(const Node* p, const Node* q, const Node* r) {
|
|
||||||
return q->x <= std::max<double>(p->x, r->x) &&
|
|
||||||
q->x >= std::min<double>(p->x, r->x) &&
|
|
||||||
q->y <= std::max<double>(p->y, r->y) &&
|
|
||||||
q->y >= std::min<double>(p->y, r->y);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename N>
|
|
||||||
int Earcut<N>::sign(double val) {
|
|
||||||
return (0.0 < val) - (val < 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a polygon diagonal intersects any polygon segments
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::intersectsPolygon(const Node* a, const Node* b) {
|
|
||||||
const Node* p = a;
|
|
||||||
do {
|
|
||||||
if (p->i != a->i && p->next->i != a->i && p->i != b->i && p->next->i != b->i &&
|
|
||||||
intersects(p, p->next, a, b)) return true;
|
|
||||||
p = p->next;
|
|
||||||
} while (p != a);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a polygon diagonal is locally inside the polygon
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::locallyInside(const Node* a, const Node* b) {
|
|
||||||
return area(a->prev, a, a->next) < 0 ?
|
|
||||||
area(a, b, a->next) >= 0 && area(a, a->prev, b) >= 0 :
|
|
||||||
area(a, b, a->prev) < 0 || area(a, a->next, b) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the middle Vertex of a polygon diagonal is inside the polygon
|
|
||||||
template <typename N>
|
|
||||||
bool Earcut<N>::middleInside(const Node* a, const Node* b) {
|
|
||||||
const Node* p = a;
|
|
||||||
bool inside = false;
|
|
||||||
double px = (a->x + b->x) / 2;
|
|
||||||
double py = (a->y + b->y) / 2;
|
|
||||||
do {
|
|
||||||
if (((p->y > py) != (p->next->y > py)) && p->next->y != p->y &&
|
|
||||||
(px < (p->next->x - p->x) * (py - p->y) / (p->next->y - p->y) + p->x))
|
|
||||||
inside = !inside;
|
|
||||||
p = p->next;
|
|
||||||
} while (p != a);
|
|
||||||
|
|
||||||
return inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits
|
|
||||||
// polygon into two; if one belongs to the outer ring and another to a hole, it merges it into a
|
|
||||||
// single ring
|
|
||||||
template <typename N>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::splitPolygon(Node* a, Node* b) {
|
|
||||||
Node* a2 = nodes.construct(a->i, a->x, a->y);
|
|
||||||
Node* b2 = nodes.construct(b->i, b->x, b->y);
|
|
||||||
Node* an = a->next;
|
|
||||||
Node* bp = b->prev;
|
|
||||||
|
|
||||||
a->next = b;
|
|
||||||
b->prev = a;
|
|
||||||
|
|
||||||
a2->next = an;
|
|
||||||
an->prev = a2;
|
|
||||||
|
|
||||||
b2->next = a2;
|
|
||||||
a2->prev = b2;
|
|
||||||
|
|
||||||
bp->next = b2;
|
|
||||||
b2->prev = bp;
|
|
||||||
|
|
||||||
return b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a node and util::optionally link it with previous one (in a circular doubly linked list)
|
|
||||||
template <typename N> template <typename Point>
|
|
||||||
typename Earcut<N>::Node*
|
|
||||||
Earcut<N>::insertNode(std::size_t i, const Point& pt, Node* last) {
|
|
||||||
Node* p = nodes.construct(static_cast<N>(i), util::nth<0, Point>::get(pt), util::nth<1, Point>::get(pt));
|
|
||||||
|
|
||||||
if (!last) {
|
|
||||||
p->prev = p;
|
|
||||||
p->next = p;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
assert(last);
|
|
||||||
p->next = last->next;
|
|
||||||
p->prev = last;
|
|
||||||
last->next->prev = p;
|
|
||||||
last->next = p;
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename N>
|
|
||||||
void Earcut<N>::removeNode(Node* p) {
|
|
||||||
p->next->prev = p->prev;
|
|
||||||
p->prev->next = p->next;
|
|
||||||
|
|
||||||
if (p->prevZ) p->prevZ->nextZ = p->nextZ;
|
|
||||||
if (p->nextZ) p->nextZ->prevZ = p->prevZ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename N = uint32_t, typename Polygon>
|
|
||||||
std::vector<N> earcut(const Polygon& poly) {
|
|
||||||
mapbox::detail::Earcut<N> earcut;
|
|
||||||
earcut(poly);
|
|
||||||
return std::move(earcut.indices);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
#ifndef LOTTIE_MESH_BINDING_H
|
|
||||||
#define LOTTIE_MESH_BINDING_H
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
typedef NS_CLOSED_ENUM(NSInteger, LottieMeshFillRule) {
|
|
||||||
LottieMeshFillRuleEvenOdd,
|
|
||||||
LottieMeshFillRuleNonZero
|
|
||||||
};
|
|
||||||
|
|
||||||
@interface LottieMeshFill : NSObject
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) LottieMeshFillRule fillRule;
|
|
||||||
|
|
||||||
- (instancetype _Nonnull)initWithFillRule:(LottieMeshFillRule)fillRule;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface LottieMeshStroke : NSObject
|
|
||||||
|
|
||||||
@property (nonatomic, readonly) CGFloat lineWidth;
|
|
||||||
@property (nonatomic, readonly) CGLineJoin lineJoin;
|
|
||||||
@property (nonatomic, readonly) CGLineCap lineCap;
|
|
||||||
@property (nonatomic, readonly) CGFloat miterLimit;
|
|
||||||
|
|
||||||
- (instancetype _Nonnull)initWithLineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface LottieMeshData : NSObject
|
|
||||||
|
|
||||||
- (NSInteger)vertexCount;
|
|
||||||
- (void)getVertexAt:(NSInteger)index x:(float * _Nullable)x y:(float * _Nullable)y;
|
|
||||||
|
|
||||||
- (NSInteger)triangleCount;
|
|
||||||
- (void * _Nonnull)getTriangles;
|
|
||||||
|
|
||||||
+ (LottieMeshData * _Nullable)generateWithPath:(UIBezierPath * _Nonnull)path fill:(LottieMeshFill * _Nullable)fill stroke:(LottieMeshStroke * _Nullable)stroke;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,315 +0,0 @@
|
|||||||
#import <LottieMeshBinding/LottieMeshBinding.h>
|
|
||||||
|
|
||||||
#import <LottieMesh/LottieMesh.h>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
MeshGenerator::Point bezierQuadraticPointAt(MeshGenerator::Point const &p0, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2, float t) {
|
|
||||||
float x = powf((1.0 - t), 2.0) * p0.x + 2.0 * (1.0 - t) * t * p1.x + powf(t, 2.0) * p2.x;
|
|
||||||
float y = powf((1.0 - t), 2.0) * p0.y + 2.0 * (1.0 - t) * t * p1.y + powf(t, 2.0) * p2.y;
|
|
||||||
return MeshGenerator::Point(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
float approximateBezierQuadraticLength(MeshGenerator::Point const &p0, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2) {
|
|
||||||
float length = 0.0f;
|
|
||||||
float t = 0.1;
|
|
||||||
MeshGenerator::Point last = p0;
|
|
||||||
while (t < 1.01) {
|
|
||||||
auto point = bezierQuadraticPointAt(p0, p1, p2, t);
|
|
||||||
length += last.distance(point);
|
|
||||||
last = point;
|
|
||||||
t += 0.1;
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
void tesselateBezier(MeshGenerator::Path &path, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2, MeshGenerator::Point const &p3, MeshGenerator::Point const &p4, int level) {
|
|
||||||
const float tessTol = 0.25f / 0.1f;
|
|
||||||
|
|
||||||
float x1 = p1.x;
|
|
||||||
float y1 = p1.y;
|
|
||||||
float x2 = p2.x;
|
|
||||||
float y2 = p2.y;
|
|
||||||
float x3 = p3.x;
|
|
||||||
float y3 = p3.y;
|
|
||||||
float x4 = p4.x;
|
|
||||||
float y4 = p4.y;
|
|
||||||
|
|
||||||
float x12, y12, x23, y23, x34, y34, x123, y123, x234, y234, x1234, y1234;
|
|
||||||
float dx, dy, d2, d3;
|
|
||||||
|
|
||||||
if (level > 10) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
x12 = (x1 + x2) * 0.5f;
|
|
||||||
y12 = (y1 + y2) * 0.5f;
|
|
||||||
x23 = (x2 + x3) * 0.5f;
|
|
||||||
y23 = (y2 + y3) * 0.5f;
|
|
||||||
x34 = (x3 + x4) * 0.5f;
|
|
||||||
y34 = (y3 + y4) * 0.5f;
|
|
||||||
x123 = (x12 + x23) * 0.5f;
|
|
||||||
y123 = (y12 + y23) * 0.5f;
|
|
||||||
|
|
||||||
dx = x4 - x1;
|
|
||||||
dy = y4 - y1;
|
|
||||||
d2 = std::abs(((x2 - x4) * dy - (y2 - y4) * dx));
|
|
||||||
d3 = std::abs(((x3 - x4) * dy - (y3 - y4) * dx));
|
|
||||||
|
|
||||||
if ((d2 + d3) * (d2 + d3) < tessTol * (dx * dx + dy * dy)) {
|
|
||||||
path.points.emplace_back(x4, y4);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
x234 = (x23+x34) * 0.5f;
|
|
||||||
y234 = (y23+y34) * 0.5f;
|
|
||||||
x1234 = (x123 + x234) * 0.5f;
|
|
||||||
y1234 = (y123 + y234) * 0.5f;
|
|
||||||
|
|
||||||
tesselateBezier(path, MeshGenerator::Point(x1, y1), MeshGenerator::Point(x12, y12), MeshGenerator::Point(x123, y123), MeshGenerator::Point(x1234, y1234), level + 1);
|
|
||||||
tesselateBezier(path, MeshGenerator::Point(x1234, y1234), MeshGenerator::Point(x234, y234), MeshGenerator::Point(x34, y34), MeshGenerator::Point(x4, y4), level + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@interface LottieMeshData () {
|
|
||||||
std::unique_ptr<MeshGenerator::Mesh> _mesh;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype _Nonnull)initWithMesh:(std::unique_ptr<MeshGenerator::Mesh> &&)mesh;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation LottieMeshData
|
|
||||||
|
|
||||||
- (instancetype _Nonnull)initWithMesh:(std::unique_ptr<MeshGenerator::Mesh> &&)mesh {
|
|
||||||
self = [super init];
|
|
||||||
if (self != nil) {
|
|
||||||
_mesh = std::move(mesh);
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)vertexCount {
|
|
||||||
return (NSInteger)_mesh->vertices.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)getVertexAt:(NSInteger)index x:(float * _Nullable)x y:(float * _Nullable)y {
|
|
||||||
MeshGenerator::Point const &point = _mesh->vertices[index];
|
|
||||||
if (x) {
|
|
||||||
*x = point.x;
|
|
||||||
}
|
|
||||||
if (y) {
|
|
||||||
*y = point.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSInteger)triangleCount {
|
|
||||||
return (NSInteger)(_mesh->triangles.size() / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void * _Nonnull)getTriangles {
|
|
||||||
return _mesh->triangles.data();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*- (void)getTriangleAt:(NSInteger)index v0:(NSInteger * _Nullable)v0 v1:(NSInteger * _Nullable)v1 v2:(NSInteger * _Nullable)v2 {
|
|
||||||
if (v0) {
|
|
||||||
*v0 = (NSInteger)_mesh->triangles[index * 3 + 0];
|
|
||||||
}
|
|
||||||
if (v1) {
|
|
||||||
*v1 = (NSInteger)_mesh->triangles[index * 3 + 1];
|
|
||||||
}
|
|
||||||
if (v2) {
|
|
||||||
*v2 = (NSInteger)_mesh->triangles[index * 3 + 2];
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
+ (LottieMeshData * _Nullable)generateWithPath:(UIBezierPath * _Nonnull)path fill: (LottieMeshFill * _Nullable)fill stroke:(LottieMeshStroke * _Nullable)stroke {
|
|
||||||
float scale = 1.0f;
|
|
||||||
float flatness = 1.0;
|
|
||||||
__block MeshGenerator::Point startingPoint(0.0f, 0.0f);
|
|
||||||
__block bool hasStartingPoint = false;
|
|
||||||
__block std::vector<MeshGenerator::Path> paths;
|
|
||||||
paths.push_back(MeshGenerator::Path());
|
|
||||||
|
|
||||||
CGPathApplyWithBlock(path.CGPath, ^(const CGPathElement * _Nonnull element) {
|
|
||||||
switch (element->type) {
|
|
||||||
case kCGPathElementMoveToPoint: {
|
|
||||||
if (!paths[paths.size() - 1].points.empty()) {
|
|
||||||
if (!paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
|
|
||||||
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
|
|
||||||
}
|
|
||||||
paths.push_back(MeshGenerator::Path());
|
|
||||||
}
|
|
||||||
|
|
||||||
startingPoint = MeshGenerator::Point((float)(element->points[0].x) * scale, (float)(element->points[0].y) * scale);
|
|
||||||
hasStartingPoint = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case kCGPathElementAddLineToPoint: {
|
|
||||||
bool canAddPoints = false;
|
|
||||||
if (paths[paths.size() - 1].points.empty()) {
|
|
||||||
if (hasStartingPoint) {
|
|
||||||
paths[paths.size() - 1].points.push_back(startingPoint);
|
|
||||||
canAddPoints = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
canAddPoints = true;
|
|
||||||
}
|
|
||||||
if (canAddPoints) {
|
|
||||||
paths[paths.size() - 1].points.push_back(MeshGenerator::Point((float)(element->points[0].x) * scale, (float)(element->points[0].y) * scale));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case kCGPathElementAddQuadCurveToPoint: {
|
|
||||||
bool canAddPoints = false;
|
|
||||||
if (paths[paths.size() - 1].points.empty()) {
|
|
||||||
if (hasStartingPoint) {
|
|
||||||
paths[paths.size() - 1].points.push_back(startingPoint);
|
|
||||||
canAddPoints = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
canAddPoints = true;
|
|
||||||
}
|
|
||||||
if (canAddPoints) {
|
|
||||||
float t = 0.001f;
|
|
||||||
|
|
||||||
MeshGenerator::Point p0 = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
|
|
||||||
MeshGenerator::Point p1(element->points[0].x * scale, element->points[0].y * scale);
|
|
||||||
MeshGenerator::Point p2(element->points[1].x * scale, element->points[1].y * scale);
|
|
||||||
|
|
||||||
float step = 10.0f * flatness / approximateBezierQuadraticLength(p0, p1, p2);
|
|
||||||
while (t < 1.0f) {
|
|
||||||
auto point = bezierQuadraticPointAt(p0, p1, p2, t);
|
|
||||||
paths[paths.size() - 1].points.push_back(point);
|
|
||||||
t += step;
|
|
||||||
}
|
|
||||||
paths[paths.size() - 1].points.push_back(p2);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case kCGPathElementAddCurveToPoint: {
|
|
||||||
bool canAddPoints = false;
|
|
||||||
if (paths[paths.size() - 1].points.empty()) {
|
|
||||||
if (hasStartingPoint) {
|
|
||||||
paths[paths.size() - 1].points.push_back(startingPoint);
|
|
||||||
canAddPoints = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
canAddPoints = true;
|
|
||||||
}
|
|
||||||
if (canAddPoints) {
|
|
||||||
float t = 0.001f;
|
|
||||||
|
|
||||||
MeshGenerator::Point p0 = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
|
|
||||||
MeshGenerator::Point p1(element->points[0].x * scale, element->points[0].y * scale);
|
|
||||||
MeshGenerator::Point p2(element->points[1].x * scale, element->points[1].y * scale);
|
|
||||||
MeshGenerator::Point p3(element->points[2].x * scale, element->points[2].y * scale);
|
|
||||||
|
|
||||||
tesselateBezier(paths[paths.size() - 1], p0, p1, p2, p3, 0);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case kCGPathElementCloseSubpath: {
|
|
||||||
if (!paths[paths.size() - 1].points.empty()) {
|
|
||||||
if (!paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
|
|
||||||
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasStartingPoint = true;
|
|
||||||
startingPoint = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
|
|
||||||
paths.push_back(MeshGenerator::Path());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!paths[paths.size() - 1].points.empty()) {
|
|
||||||
if (stroke == nil && !paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
|
|
||||||
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
paths.pop_back();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<MeshGenerator::Fill> mappedFill;
|
|
||||||
if (fill) {
|
|
||||||
mappedFill = std::make_unique<MeshGenerator::Fill>(fill.fillRule == LottieMeshFillRuleEvenOdd ? MeshGenerator::Fill::Rule::EvenOdd : MeshGenerator::Fill::Rule::NonZero);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<MeshGenerator::Stroke> mappedStroke;
|
|
||||||
if (stroke) {
|
|
||||||
MeshGenerator::Stroke::LineJoin lineJoin;
|
|
||||||
switch (stroke.lineJoin) {
|
|
||||||
case kCGLineJoinRound:
|
|
||||||
lineJoin = MeshGenerator::Stroke::LineJoin::Round;
|
|
||||||
break;
|
|
||||||
case kCGLineJoinBevel:
|
|
||||||
lineJoin = MeshGenerator::Stroke::LineJoin::Bevel;
|
|
||||||
break;
|
|
||||||
case kCGLineJoinMiter:
|
|
||||||
lineJoin = MeshGenerator::Stroke::LineJoin::Miter;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
lineJoin = MeshGenerator::Stroke::LineJoin::Round;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
MeshGenerator::Stroke::LineCap lineCap;
|
|
||||||
switch (stroke.lineCap) {
|
|
||||||
case kCGLineCapRound:
|
|
||||||
lineCap = MeshGenerator::Stroke::LineCap::Round;
|
|
||||||
break;
|
|
||||||
case kCGLineCapButt:
|
|
||||||
lineCap = MeshGenerator::Stroke::LineCap::Butt;
|
|
||||||
break;
|
|
||||||
case kCGLineCapSquare:
|
|
||||||
lineCap = MeshGenerator::Stroke::LineCap::Square;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
lineCap = MeshGenerator::Stroke::LineCap::Round;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedStroke = std::make_unique<MeshGenerator::Stroke>((float)stroke.lineWidth, lineJoin, lineCap, (float)stroke.miterLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<MeshGenerator::Mesh> resultMesh = MeshGenerator::generateMesh(paths, std::move(mappedFill), std::move(mappedStroke));
|
|
||||||
if (resultMesh) {
|
|
||||||
return [[LottieMeshData alloc] initWithMesh:std::move(resultMesh)];
|
|
||||||
} else {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation LottieMeshFill
|
|
||||||
|
|
||||||
- (instancetype _Nonnull)initWithFillRule:(LottieMeshFillRule)fillRule {
|
|
||||||
self = [super init];
|
|
||||||
if (self != nil) {
|
|
||||||
_fillRule = fillRule;
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation LottieMeshStroke
|
|
||||||
|
|
||||||
- (instancetype _Nonnull)initWithLineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit {
|
|
||||||
self = [super init];
|
|
||||||
if (self != nil) {
|
|
||||||
_lineWidth = lineWidth;
|
|
||||||
_lineJoin = lineJoin;
|
|
||||||
_lineCap = lineCap;
|
|
||||||
_miterLimit = miterLimit;
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
@ -1,82 +0,0 @@
|
|||||||
#include <metal_stdlib>
|
|
||||||
using namespace metal;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
packed_float2 position;
|
|
||||||
} Vertex;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
packed_float2 offset;
|
|
||||||
} Offset;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
float4 position[[position]];
|
|
||||||
float2 localPosition[[center_no_perspective]];
|
|
||||||
} Varyings;
|
|
||||||
|
|
||||||
float2 screenSpaceToRelative(float2 point, float2 viewSize) {
|
|
||||||
float2 inverseViewSize = 1 / viewSize;
|
|
||||||
float clipX = (2.0f * point.x * inverseViewSize.x) - 2.0f;
|
|
||||||
float clipY = (2.0f * -point.y * inverseViewSize.y) + 2.0f;
|
|
||||||
|
|
||||||
return float2(clipX, clipY);
|
|
||||||
}
|
|
||||||
|
|
||||||
vertex Varyings vertexPassthrough(
|
|
||||||
constant Vertex *verticies[[buffer(0)]],
|
|
||||||
constant float2 &offset[[buffer(1)]],
|
|
||||||
unsigned int vid[[vertex_id]],
|
|
||||||
constant float4x4 &transformMatrix[[buffer(2)]],
|
|
||||||
constant int &indexOffset[[buffer(3)]]
|
|
||||||
) {
|
|
||||||
Varyings out;
|
|
||||||
constant Vertex &v = verticies[vid + indexOffset];
|
|
||||||
float2 viewSize(512.0f, 512.0f);
|
|
||||||
float4 transformedVertex = transformMatrix * float4(v.position, 0.0, 1.0);
|
|
||||||
out.position = float4(screenSpaceToRelative(float2(transformedVertex.x, transformedVertex.y) + offset, viewSize), 0.0, 1.0);
|
|
||||||
out.localPosition = float2(v.position);
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment half4 fragmentPassthrough(
|
|
||||||
Varyings in[[stage_in]],
|
|
||||||
constant float4 &color[[buffer(1)]]
|
|
||||||
) {
|
|
||||||
float4 out = color;
|
|
||||||
|
|
||||||
return half4(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<int N>
|
|
||||||
half4 mixGradientColors(float dist, constant float4 *colors, constant float *steps) {
|
|
||||||
float4 color = colors[0];
|
|
||||||
for (int i = 1; i < N; i++) {
|
|
||||||
color = mix(color, colors[i], smoothstep(steps[i - 1], steps[i], dist));
|
|
||||||
}
|
|
||||||
|
|
||||||
return half4(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#define radialGradientFunc(N) fragment half4 fragmentRadialGradient##N( \
|
|
||||||
Varyings in[[stage_in]], \
|
|
||||||
constant float2 &start[[buffer(1)]], \
|
|
||||||
constant float2 &end[[buffer(2)]], \
|
|
||||||
constant float4 *colors[[buffer(3)]], \
|
|
||||||
constant float *steps[[buffer(4)]] \
|
|
||||||
) { \
|
|
||||||
float centerDistance = distance(in.localPosition, start); \
|
|
||||||
float endDistance = distance(start, end); \
|
|
||||||
float dist = min(1.0, centerDistance / endDistance); \
|
|
||||||
return mixGradientColors<N>(dist, colors, steps); \
|
|
||||||
}
|
|
||||||
|
|
||||||
radialGradientFunc(2)
|
|
||||||
radialGradientFunc(3)
|
|
||||||
radialGradientFunc(4)
|
|
||||||
radialGradientFunc(5)
|
|
||||||
radialGradientFunc(6)
|
|
||||||
radialGradientFunc(7)
|
|
||||||
radialGradientFunc(8)
|
|
||||||
radialGradientFunc(9)
|
|
||||||
radialGradientFunc(10)
|
|
@ -1,122 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Postbox
|
|
||||||
import ManagedFile
|
|
||||||
|
|
||||||
private let emptyMemory = malloc(1)!
|
|
||||||
|
|
||||||
public class MeshMemoryBuffer {
|
|
||||||
public internal(set) var data: Data
|
|
||||||
public internal(set) var length: Int
|
|
||||||
|
|
||||||
public init(data: Data) {
|
|
||||||
self.data = data
|
|
||||||
self.length = data.count
|
|
||||||
}
|
|
||||||
|
|
||||||
public func makeData() -> Data {
|
|
||||||
if self.data.count == self.length {
|
|
||||||
return self.data
|
|
||||||
} else {
|
|
||||||
return self.data.subdata(in: 0 ..< self.length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension WriteBuffer {
|
|
||||||
func writeInt32(_ value: Int32) {
|
|
||||||
var value = value
|
|
||||||
self.write(&value, length: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeFloat(_ value: Float) {
|
|
||||||
var value: Float32 = value
|
|
||||||
self.write(&value, length: 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class MeshWriteBuffer {
|
|
||||||
let file: ManagedFile
|
|
||||||
private(set) var offset: Int = 0
|
|
||||||
|
|
||||||
public init(file: ManagedFile) {
|
|
||||||
self.file = file
|
|
||||||
}
|
|
||||||
|
|
||||||
public func write(_ data: UnsafeRawPointer, length: Int) {
|
|
||||||
let _ = self.file.write(data, count: length)
|
|
||||||
self.offset += length
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeInt8(_ value: Int8) {
|
|
||||||
var value = value
|
|
||||||
self.write(&value, length: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeInt32(_ value: Int32) {
|
|
||||||
var value = value
|
|
||||||
self.write(&value, length: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeFloat(_ value: Float) {
|
|
||||||
var value: Float32 = value
|
|
||||||
self.write(&value, length: 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func write(_ data: Data) {
|
|
||||||
data.withUnsafeBytes { bytes in
|
|
||||||
self.write(bytes.baseAddress!, length: bytes.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func write(_ data: DataRange) {
|
|
||||||
data.data.withUnsafeBytes { bytes in
|
|
||||||
self.write(bytes.baseAddress!.advanced(by: data.range.lowerBound), length: data.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func seek(offset: Int) {
|
|
||||||
let _ = self.file.seek(position: Int64(offset))
|
|
||||||
self.offset = offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class MeshReadBuffer: MeshMemoryBuffer {
|
|
||||||
public var offset = 0
|
|
||||||
|
|
||||||
override public init(data: Data) {
|
|
||||||
super.init(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func read(_ data: UnsafeMutableRawPointer, length: Int) {
|
|
||||||
self.data.copyBytes(to: data.assumingMemoryBound(to: UInt8.self), from: self.offset ..< (self.offset + length))
|
|
||||||
self.offset += length
|
|
||||||
}
|
|
||||||
|
|
||||||
func readDataRange(count: Int) -> DataRange {
|
|
||||||
let result = DataRange(data: self.data, range: self.offset ..< (self.offset + count))
|
|
||||||
self.offset += count
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
public func readInt8() -> Int8 {
|
|
||||||
var result: Int8 = 0
|
|
||||||
self.read(&result, length: 1)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
public func readInt32() -> Int32 {
|
|
||||||
var result: Int32 = 0
|
|
||||||
self.read(&result, length: 4)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
public func readFloat() -> Float {
|
|
||||||
var result: Float32 = 0
|
|
||||||
self.read(&result, length: 4)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
public func skip(_ length: Int) {
|
|
||||||
self.offset += length
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class CapturedGeometryNode {
|
|
||||||
final class DisplayItem {
|
|
||||||
enum Display {
|
|
||||||
enum Style {
|
|
||||||
enum GradientType {
|
|
||||||
case linear
|
|
||||||
case radial
|
|
||||||
}
|
|
||||||
|
|
||||||
case color(color: UIColor, alpha: CGFloat)
|
|
||||||
case gradient(colors: [UIColor], positions: [CGFloat], start: CGPoint, end: CGPoint, type: GradientType)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Fill {
|
|
||||||
var style: Style
|
|
||||||
var fillRule: CGPathFillRule
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Stroke {
|
|
||||||
var style: Style
|
|
||||||
var lineWidth: CGFloat
|
|
||||||
var lineCap: CGLineCap
|
|
||||||
var lineJoin: CGLineJoin
|
|
||||||
var miterLimit: CGFloat
|
|
||||||
}
|
|
||||||
|
|
||||||
case fill(Fill)
|
|
||||||
case stroke(Stroke)
|
|
||||||
}
|
|
||||||
|
|
||||||
let path: CGPath
|
|
||||||
let display: Display
|
|
||||||
|
|
||||||
init(path: CGPath, display: Display) {
|
|
||||||
self.path = path
|
|
||||||
self.display = display
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var transform: CATransform3D
|
|
||||||
let alpha: CGFloat
|
|
||||||
let isHidden: Bool
|
|
||||||
let displayItem: DisplayItem?
|
|
||||||
let subnodes: [CapturedGeometryNode]
|
|
||||||
|
|
||||||
init(
|
|
||||||
transform: CATransform3D,
|
|
||||||
alpha: CGFloat,
|
|
||||||
isHidden: Bool,
|
|
||||||
displayItem: DisplayItem?,
|
|
||||||
subnodes: [CapturedGeometryNode]
|
|
||||||
) {
|
|
||||||
self.transform = transform
|
|
||||||
self.alpha = alpha
|
|
||||||
self.isHidden = isHidden
|
|
||||||
self.displayItem = displayItem
|
|
||||||
self.subnodes = subnodes
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import QuartzCore
|
|
||||||
|
|
||||||
/**
|
|
||||||
The base class for a child layer of CompositionContainer
|
|
||||||
*/
|
|
||||||
class MyCompositionLayer {
|
|
||||||
var bounds: CGRect = CGRect()
|
|
||||||
|
|
||||||
let transformNode: LayerTransformNode
|
|
||||||
|
|
||||||
//let contentsLayer: CALayer = CALayer()
|
|
||||||
|
|
||||||
let maskLayer: MyMaskContainerLayer?
|
|
||||||
|
|
||||||
let matteType: MatteType?
|
|
||||||
|
|
||||||
var matteLayer: MyCompositionLayer? {
|
|
||||||
didSet {
|
|
||||||
//NOTE
|
|
||||||
/*if let matte = matteLayer {
|
|
||||||
if let type = matteType, type == .invert {
|
|
||||||
|
|
||||||
mask = InvertedMatteLayer(inputMatte: matte)
|
|
||||||
} else {
|
|
||||||
mask = matte
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mask = nil
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inFrame: CGFloat
|
|
||||||
let outFrame: CGFloat
|
|
||||||
let startFrame: CGFloat
|
|
||||||
let timeStretch: CGFloat
|
|
||||||
|
|
||||||
init(layer: LayerModel, size: CGSize) {
|
|
||||||
self.transformNode = LayerTransformNode(transform: layer.transform)
|
|
||||||
if let masks = layer.masks {
|
|
||||||
maskLayer = MyMaskContainerLayer(masks: masks)
|
|
||||||
} else {
|
|
||||||
maskLayer = nil
|
|
||||||
}
|
|
||||||
self.matteType = layer.matte
|
|
||||||
self.inFrame = layer.inFrame.cgFloat
|
|
||||||
self.outFrame = layer.outFrame.cgFloat
|
|
||||||
self.timeStretch = layer.timeStretch.cgFloat
|
|
||||||
self.startFrame = layer.startTime.cgFloat
|
|
||||||
|
|
||||||
//NOTE
|
|
||||||
//self.anchorPoint = .zero
|
|
||||||
|
|
||||||
//NOTE
|
|
||||||
/*contentsLayer.anchorPoint = .zero
|
|
||||||
contentsLayer.bounds = CGRect(origin: .zero, size: size)
|
|
||||||
contentsLayer.actions = [
|
|
||||||
"opacity" : NSNull(),
|
|
||||||
"transform" : NSNull(),
|
|
||||||
"bounds" : NSNull(),
|
|
||||||
"anchorPoint" : NSNull(),
|
|
||||||
"sublayerTransform" : NSNull(),
|
|
||||||
"hidden" : NSNull()
|
|
||||||
]
|
|
||||||
addSublayer(contentsLayer)
|
|
||||||
|
|
||||||
if let maskLayer = maskLayer {
|
|
||||||
contentsLayer.mask = maskLayer
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private(set) var isHidden = false
|
|
||||||
|
|
||||||
final func displayWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
|
||||||
transformNode.updateTree(frame, forceUpdates: forceUpdates)
|
|
||||||
let layerVisible = frame.isInRangeOrEqual(inFrame, outFrame)
|
|
||||||
/// Only update contents if current time is within the layers time bounds.
|
|
||||||
if layerVisible {
|
|
||||||
displayContentsWithFrame(frame: frame, forceUpdates: forceUpdates)
|
|
||||||
maskLayer?.updateWithFrame(frame: frame, forceUpdates: forceUpdates)
|
|
||||||
}
|
|
||||||
self.isHidden = !layerVisible
|
|
||||||
//NOTE
|
|
||||||
/*contentsLayer.transform = transformNode.globalTransform
|
|
||||||
contentsLayer.opacity = transformNode.opacity
|
|
||||||
contentsLayer.isHidden = !layerVisible*/
|
|
||||||
}
|
|
||||||
|
|
||||||
func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
|
||||||
/// To be overridden by subclass
|
|
||||||
}
|
|
||||||
|
|
||||||
func captureGeometry() -> CapturedGeometryNode {
|
|
||||||
return CapturedGeometryNode(
|
|
||||||
transform: self.transformNode.globalTransform,
|
|
||||||
alpha: CGFloat(self.transformNode.opacity),
|
|
||||||
isHidden: self.isHidden,
|
|
||||||
displayItem: self.captureDisplayItem(),
|
|
||||||
subnodes: self.captureChildren()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
|
|
||||||
func captureChildren() -> [CapturedGeometryNode] {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CoreGraphics
|
|
||||||
import QuartzCore
|
|
||||||
|
|
||||||
final class MyImageCompositionLayer: MyCompositionLayer {
|
|
||||||
|
|
||||||
var image: CGImage? = nil {
|
|
||||||
didSet {
|
|
||||||
//NOTE
|
|
||||||
/*if let image = image {
|
|
||||||
contentsLayer.contents = image
|
|
||||||
} else {
|
|
||||||
contentsLayer.contents = nil
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageReferenceID: String
|
|
||||||
|
|
||||||
init(imageLayer: ImageLayerModel, size: CGSize) {
|
|
||||||
self.imageReferenceID = imageLayer.referenceID
|
|
||||||
super.init(layer: imageLayer, size: size)
|
|
||||||
|
|
||||||
//NOTE
|
|
||||||
//contentsLayer.masksToBounds = true
|
|
||||||
//contentsLayer.contentsGravity = CALayerContentsGravity.resize
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureChildren() -> [CapturedGeometryNode] {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import QuartzCore
|
|
||||||
|
|
||||||
extension MaskMode {
|
|
||||||
var usableMode: MaskMode {
|
|
||||||
switch self {
|
|
||||||
case .add:
|
|
||||||
return .add
|
|
||||||
case .subtract:
|
|
||||||
return .subtract
|
|
||||||
case .intersect:
|
|
||||||
return .intersect
|
|
||||||
case .lighten:
|
|
||||||
return .add
|
|
||||||
case .darken:
|
|
||||||
return .darken
|
|
||||||
case .difference:
|
|
||||||
return .intersect
|
|
||||||
case .none:
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CGRect {
|
|
||||||
static var veryLargeRect: CGRect {
|
|
||||||
return CGRect(x: -100_000_000,
|
|
||||||
y: -100_000_000,
|
|
||||||
width: 200_000_000,
|
|
||||||
height: 200_000_000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MyMaskContainerLayer {
|
|
||||||
|
|
||||||
init(masks: [Mask]) {
|
|
||||||
//NOTE
|
|
||||||
//anchorPoint = .zero
|
|
||||||
var containerLayer = CALayer()
|
|
||||||
var firstObject: Bool = true
|
|
||||||
for mask in masks {
|
|
||||||
let maskLayer = MaskLayer(mask: mask)
|
|
||||||
maskLayers.append(maskLayer)
|
|
||||||
if mask.mode.usableMode == .none {
|
|
||||||
continue
|
|
||||||
} else if mask.mode.usableMode == .add || firstObject {
|
|
||||||
firstObject = false
|
|
||||||
containerLayer.addSublayer(maskLayer)
|
|
||||||
} else {
|
|
||||||
containerLayer.mask = maskLayer
|
|
||||||
let newContainer = CALayer()
|
|
||||||
newContainer.addSublayer(containerLayer)
|
|
||||||
containerLayer = newContainer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//NOTE
|
|
||||||
//addSublayer(containerLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate var maskLayers: [MaskLayer] = []
|
|
||||||
|
|
||||||
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
|
||||||
maskLayers.forEach({ $0.updateWithFrame(frame: frame, forceUpdates: forceUpdates) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate class MaskLayer: CALayer {
|
|
||||||
|
|
||||||
let properties: MaskNodeProperties?
|
|
||||||
|
|
||||||
let maskLayer = CAShapeLayer()
|
|
||||||
|
|
||||||
init(mask: Mask) {
|
|
||||||
self.properties = MaskNodeProperties(mask: mask)
|
|
||||||
super.init()
|
|
||||||
addSublayer(maskLayer)
|
|
||||||
anchorPoint = .zero
|
|
||||||
maskLayer.fillColor = mask.mode == .add ? CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 1]) :
|
|
||||||
CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 1])
|
|
||||||
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
|
|
||||||
self.actions = [
|
|
||||||
"opacity" : NSNull()
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(layer: Any) {
|
|
||||||
self.properties = nil
|
|
||||||
super.init(layer: layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
|
||||||
guard let properties = properties else { return }
|
|
||||||
if properties.opacity.needsUpdate(frame: frame) || forceUpdates {
|
|
||||||
properties.opacity.update(frame: frame)
|
|
||||||
self.opacity = Float(properties.opacity.value.cgFloatValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if properties.shape.needsUpdate(frame: frame) || forceUpdates {
|
|
||||||
properties.shape.update(frame: frame)
|
|
||||||
properties.expansion.update(frame: frame)
|
|
||||||
|
|
||||||
let shapePath = properties.shape.value.cgPath()
|
|
||||||
var path = shapePath
|
|
||||||
if properties.mode.usableMode == .subtract && !properties.inverted ||
|
|
||||||
(properties.mode.usableMode == .add && properties.inverted) {
|
|
||||||
/// Add a bounds rect to invert the mask
|
|
||||||
let newPath = CGMutablePath()
|
|
||||||
newPath.addRect(CGRect.veryLargeRect)
|
|
||||||
newPath.addPath(shapePath)
|
|
||||||
path = newPath
|
|
||||||
}
|
|
||||||
maskLayer.path = path
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate class MaskNodeProperties: NodePropertyMap {
|
|
||||||
|
|
||||||
var propertyMap: [String : AnyNodeProperty]
|
|
||||||
|
|
||||||
var properties: [AnyNodeProperty]
|
|
||||||
|
|
||||||
init(mask: Mask) {
|
|
||||||
self.mode = mask.mode
|
|
||||||
self.inverted = mask.inverted
|
|
||||||
self.opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.opacity.keyframes))
|
|
||||||
self.shape = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.shape.keyframes))
|
|
||||||
self.expansion = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.expansion.keyframes))
|
|
||||||
self.propertyMap = [
|
|
||||||
"Opacity" : opacity,
|
|
||||||
"Shape" : shape,
|
|
||||||
"Expansion" : expansion
|
|
||||||
]
|
|
||||||
self.properties = Array(self.propertyMap.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
let mode: MaskMode
|
|
||||||
let inverted: Bool
|
|
||||||
|
|
||||||
let opacity: NodeProperty<Vector1D>
|
|
||||||
let shape: NodeProperty<BezierPath>
|
|
||||||
let expansion: NodeProperty<Vector1D>
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
final class MyNullCompositionLayer: MyCompositionLayer {
|
|
||||||
init(layer: LayerModel) {
|
|
||||||
super.init(layer: layer, size: .zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureChildren() -> [CapturedGeometryNode] {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import QuartzCore
|
|
||||||
|
|
||||||
final class MyPreCompositionLayer: MyCompositionLayer {
|
|
||||||
|
|
||||||
let frameRate: CGFloat
|
|
||||||
let remappingNode: NodeProperty<Vector1D>?
|
|
||||||
fileprivate var animationLayers: [MyCompositionLayer]
|
|
||||||
|
|
||||||
init(precomp: PreCompLayerModel,
|
|
||||||
asset: PrecompAsset,
|
|
||||||
assetLibrary: AssetLibrary?,
|
|
||||||
frameRate: CGFloat) {
|
|
||||||
self.animationLayers = []
|
|
||||||
if let keyframes = precomp.timeRemapping?.keyframes {
|
|
||||||
self.remappingNode = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframes))
|
|
||||||
} else {
|
|
||||||
self.remappingNode = nil
|
|
||||||
}
|
|
||||||
self.frameRate = frameRate
|
|
||||||
super.init(layer: precomp, size: CGSize(width: precomp.width, height: precomp.height))
|
|
||||||
bounds = CGRect(origin: .zero, size: CGSize(width: precomp.width, height: precomp.height))
|
|
||||||
|
|
||||||
//NOTE
|
|
||||||
//contentsLayer.masksToBounds = true
|
|
||||||
//contentsLayer.bounds = bounds
|
|
||||||
|
|
||||||
let layers = initializeCompositionLayers(layers: asset.layers, assetLibrary: assetLibrary, frameRate: frameRate)
|
|
||||||
|
|
||||||
var imageLayers = [MyImageCompositionLayer]()
|
|
||||||
|
|
||||||
var mattedLayer: MyCompositionLayer? = nil
|
|
||||||
|
|
||||||
for layer in layers.reversed() {
|
|
||||||
layer.bounds = bounds
|
|
||||||
//NOTE
|
|
||||||
animationLayers.append(layer)
|
|
||||||
if let imageLayer = layer as? MyImageCompositionLayer {
|
|
||||||
imageLayers.append(imageLayer)
|
|
||||||
}
|
|
||||||
if let matte = mattedLayer {
|
|
||||||
/// The previous layer requires this layer to be its matte
|
|
||||||
matte.matteLayer = layer
|
|
||||||
mattedLayer = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if let matte = layer.matteType,
|
|
||||||
(matte == .add || matte == .invert) {
|
|
||||||
/// We have a layer that requires a matte.
|
|
||||||
mattedLayer = layer
|
|
||||||
}
|
|
||||||
//NOTE
|
|
||||||
//contentsLayer.addSublayer(layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
//NOTE
|
|
||||||
//layerImageProvider.addImageLayers(imageLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
|
||||||
let localFrame: CGFloat
|
|
||||||
if let remappingNode = remappingNode {
|
|
||||||
remappingNode.update(frame: frame)
|
|
||||||
localFrame = remappingNode.value.cgFloatValue * frameRate
|
|
||||||
} else {
|
|
||||||
localFrame = (frame - startFrame) / timeStretch
|
|
||||||
}
|
|
||||||
for animationLayer in self.animationLayers {
|
|
||||||
animationLayer.displayWithFrame(frame: localFrame, forceUpdates: forceUpdates)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureChildren() -> [CapturedGeometryNode] {
|
|
||||||
var result: [CapturedGeometryNode] = []
|
|
||||||
for animationLayer in self.animationLayers {
|
|
||||||
result.append(animationLayer.captureGeometry())
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CoreGraphics
|
|
||||||
|
|
||||||
/**
|
|
||||||
A CompositionLayer responsible for initializing and rendering shapes
|
|
||||||
*/
|
|
||||||
final class MyShapeCompositionLayer: MyCompositionLayer {
|
|
||||||
|
|
||||||
let rootNode: AnimatorNode?
|
|
||||||
let renderContainer: ShapeContainerLayer?
|
|
||||||
|
|
||||||
init(shapeLayer: ShapeLayerModel) {
|
|
||||||
let results = shapeLayer.items.initializeNodeTree()
|
|
||||||
let renderContainer = ShapeContainerLayer()
|
|
||||||
self.renderContainer = renderContainer
|
|
||||||
self.rootNode = results.rootNode
|
|
||||||
super.init(layer: shapeLayer, size: .zero)
|
|
||||||
|
|
||||||
//NOTE
|
|
||||||
//contentsLayer.addSublayer(renderContainer)
|
|
||||||
for container in results.renderContainers {
|
|
||||||
renderContainer.insertRenderLayer(container)
|
|
||||||
}
|
|
||||||
rootNode?.updateTree(0, forceUpdates: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
|
||||||
rootNode?.updateTree(frame, forceUpdates: forceUpdates)
|
|
||||||
renderContainer?.markRenderUpdates(forFrame: frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureGeometry() -> CapturedGeometryNode {
|
|
||||||
var subnodes: [CapturedGeometryNode] = []
|
|
||||||
if let renderContainer = self.renderContainer {
|
|
||||||
subnodes.append(renderContainer.captureGeometry())
|
|
||||||
}
|
|
||||||
|
|
||||||
return CapturedGeometryNode(
|
|
||||||
transform: self.transformNode.globalTransform,
|
|
||||||
alpha: CGFloat(self.transformNode.opacity),
|
|
||||||
isHidden: self.isHidden,
|
|
||||||
displayItem: nil,
|
|
||||||
subnodes: subnodes
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureChildren() -> [CapturedGeometryNode] {
|
|
||||||
preconditionFailure()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class MySolidCompositionLayer: MyCompositionLayer {
|
|
||||||
let colorProperty: NodeProperty<Color>?
|
|
||||||
let solidShape: CAShapeLayer = CAShapeLayer()
|
|
||||||
|
|
||||||
init(solid: SolidLayerModel) {
|
|
||||||
let components = solid.colorHex.hexColorComponents()
|
|
||||||
self.colorProperty = NodeProperty(provider: SingleValueProvider(Color(r: Double(components.red), g: Double(components.green), b: Double(components.blue), a: 1)))
|
|
||||||
|
|
||||||
super.init(layer: solid, size: .zero)
|
|
||||||
solidShape.path = CGPath(rect: CGRect(x: 0, y: 0, width: solid.width, height: solid.height), transform: nil)
|
|
||||||
//NOTE
|
|
||||||
//contentsLayer.addSublayer(solidShape)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
|
|
||||||
guard let colorProperty = colorProperty else { return }
|
|
||||||
colorProperty.update(frame: frame)
|
|
||||||
solidShape.fillColor = colorProperty.value.cgColorValue
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
|
|
||||||
guard let colorProperty = colorProperty else { return nil }
|
|
||||||
guard let path = self.solidShape.path else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return CapturedGeometryNode.DisplayItem(
|
|
||||||
path: path,
|
|
||||||
display: .fill(CapturedGeometryNode.DisplayItem.Display.Fill(
|
|
||||||
style: .color(color: UIColor(cgColor: colorProperty.value.cgColorValue), alpha: 1.0),
|
|
||||||
fillRule: .evenOdd
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func captureChildren() -> [CapturedGeometryNode] {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
//
|
|
||||||
// InvertedMatteLayer.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/28/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import QuartzCore
|
|
||||||
|
|
||||||
/**
|
|
||||||
A layer that inverses the alpha output of its input layer.
|
|
||||||
|
|
||||||
WARNING: This is experimental and probably not very performant.
|
|
||||||
*/
|
|
||||||
/*final class InvertedMatteLayer: CALayer, CompositionLayerDelegate {
|
|
||||||
|
|
||||||
let inputMatte: CompositionLayer?
|
|
||||||
let wrapperLayer = CALayer()
|
|
||||||
|
|
||||||
init(inputMatte: CompositionLayer) {
|
|
||||||
self.inputMatte = inputMatte
|
|
||||||
super.init()
|
|
||||||
inputMatte.layerDelegate = self
|
|
||||||
self.anchorPoint = .zero
|
|
||||||
self.bounds = inputMatte.bounds
|
|
||||||
self.setNeedsDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(layer: Any) {
|
|
||||||
guard let layer = layer as? InvertedMatteLayer else {
|
|
||||||
fatalError("init(layer:) wrong class.")
|
|
||||||
}
|
|
||||||
self.inputMatte = nil
|
|
||||||
super.init(layer: layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frameUpdated(frame: CGFloat) {
|
|
||||||
self.setNeedsDisplay()
|
|
||||||
self.displayIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func draw(in ctx: CGContext) {
|
|
||||||
guard let inputMatte = inputMatte else { return }
|
|
||||||
guard let fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1])
|
|
||||||
else { return }
|
|
||||||
ctx.setFillColor(fillColor)
|
|
||||||
ctx.fill(bounds)
|
|
||||||
ctx.setBlendMode(.destinationOut)
|
|
||||||
inputMatte.render(in: ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
@ -1,120 +0,0 @@
|
|||||||
//
|
|
||||||
// LayerTransformPropertyMap.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 2/4/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CoreGraphics
|
|
||||||
import QuartzCore
|
|
||||||
|
|
||||||
final class LayerTransformProperties: NodePropertyMap {
|
|
||||||
|
|
||||||
init(transform: Transform) {
|
|
||||||
|
|
||||||
self.anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes))
|
|
||||||
self.scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
|
|
||||||
self.rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
|
|
||||||
self.opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
|
|
||||||
|
|
||||||
var propertyMap: [String: AnyNodeProperty] = [
|
|
||||||
"Anchor Point" : anchor,
|
|
||||||
"Scale" : scale,
|
|
||||||
"Rotation" : rotation,
|
|
||||||
"Opacity" : opacity
|
|
||||||
]
|
|
||||||
|
|
||||||
if let positionKeyframesX = transform.positionX?.keyframes,
|
|
||||||
let positionKeyframesY = transform.positionY?.keyframes {
|
|
||||||
let xPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesX))
|
|
||||||
let yPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesY))
|
|
||||||
propertyMap["X Position"] = xPosition
|
|
||||||
propertyMap["Y Position"] = yPosition
|
|
||||||
self.positionX = xPosition
|
|
||||||
self.positionY = yPosition
|
|
||||||
self.position = nil
|
|
||||||
} else if let positionKeyframes = transform.position?.keyframes {
|
|
||||||
let position: NodeProperty<Vector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
|
|
||||||
propertyMap["Position"] = position
|
|
||||||
self.position = position
|
|
||||||
self.positionX = nil
|
|
||||||
self.positionY = nil
|
|
||||||
} else {
|
|
||||||
self.position = nil
|
|
||||||
self.positionY = nil
|
|
||||||
self.positionX = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.properties = Array(propertyMap.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
let properties: [AnyNodeProperty]
|
|
||||||
|
|
||||||
let anchor: NodeProperty<Vector3D>
|
|
||||||
let scale: NodeProperty<Vector3D>
|
|
||||||
let rotation: NodeProperty<Vector1D>
|
|
||||||
let position: NodeProperty<Vector3D>?
|
|
||||||
let positionX: NodeProperty<Vector1D>?
|
|
||||||
let positionY: NodeProperty<Vector1D>?
|
|
||||||
let opacity: NodeProperty<Vector1D>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class LayerTransformNode: AnimatorNode {
|
|
||||||
let outputNode: NodeOutput = PassThroughOutputNode(parent: nil)
|
|
||||||
|
|
||||||
init(transform: Transform) {
|
|
||||||
self.transformProperties = LayerTransformProperties(transform: transform)
|
|
||||||
}
|
|
||||||
|
|
||||||
let transformProperties: LayerTransformProperties
|
|
||||||
|
|
||||||
// MARK: Animator Node Protocol
|
|
||||||
|
|
||||||
var propertyMap: NodePropertyMap {
|
|
||||||
return transformProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
var parentNode: AnimatorNode?
|
|
||||||
var hasLocalUpdates: Bool = false
|
|
||||||
var hasUpstreamUpdates: Bool = false
|
|
||||||
var lastUpdateFrame: CGFloat? = nil
|
|
||||||
var isEnabled: Bool = true
|
|
||||||
|
|
||||||
func shouldRebuildOutputs(frame: CGFloat) -> Bool {
|
|
||||||
return hasLocalUpdates || hasUpstreamUpdates
|
|
||||||
}
|
|
||||||
|
|
||||||
func rebuildOutputs(frame: CGFloat) {
|
|
||||||
opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
|
|
||||||
|
|
||||||
let position: CGPoint
|
|
||||||
if let point = transformProperties.position?.value.pointValue {
|
|
||||||
position = point
|
|
||||||
} else if let xPos = transformProperties.positionX?.value.cgFloatValue,
|
|
||||||
let yPos = transformProperties.positionY?.value.cgFloatValue {
|
|
||||||
position = CGPoint(x: xPos, y: yPos)
|
|
||||||
} else {
|
|
||||||
position = .zero
|
|
||||||
}
|
|
||||||
|
|
||||||
localTransform = CATransform3D.makeTransform(anchor: transformProperties.anchor.value.pointValue,
|
|
||||||
position: position,
|
|
||||||
scale: transformProperties.scale.value.sizeValue,
|
|
||||||
rotation: transformProperties.rotation.value.cgFloatValue,
|
|
||||||
skew: nil,
|
|
||||||
skewAxis: nil)
|
|
||||||
|
|
||||||
if let parentNode = parentNode as? LayerTransformNode {
|
|
||||||
globalTransform = CATransform3DConcat(localTransform, parentNode.globalTransform)
|
|
||||||
} else {
|
|
||||||
globalTransform = localTransform
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var opacity: Float = 1
|
|
||||||
var localTransform: CATransform3D = CATransform3DIdentity
|
|
||||||
var globalTransform: CATransform3D = CATransform3DIdentity
|
|
||||||
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
//
|
|
||||||
// Animation.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/7/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public enum CoordinateSpace: Int, Codable {
|
|
||||||
case type2d
|
|
||||||
case type3d
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
The `Animation` model is the top level model object in Lottie.
|
|
||||||
|
|
||||||
An `Animation` holds all of the animation data backing a Lottie Animation.
|
|
||||||
Codable, see JSON schema [here](https://github.com/airbnb/lottie-web/tree/master/docs/json).
|
|
||||||
*/
|
|
||||||
public final class Animation: Codable {
|
|
||||||
|
|
||||||
/// The version of the JSON Schema.
|
|
||||||
let version: String
|
|
||||||
|
|
||||||
/// The coordinate space of the composition.
|
|
||||||
let type: CoordinateSpace
|
|
||||||
|
|
||||||
/// The start time of the composition in frameTime.
|
|
||||||
public let startFrame: AnimationFrameTime
|
|
||||||
|
|
||||||
/// The end time of the composition in frameTime.
|
|
||||||
public let endFrame: AnimationFrameTime
|
|
||||||
|
|
||||||
/// The frame rate of the composition.
|
|
||||||
public let framerate: Double
|
|
||||||
|
|
||||||
/// The height of the composition in points.
|
|
||||||
let width: Int
|
|
||||||
|
|
||||||
/// The width of the composition in points.
|
|
||||||
let height: Int
|
|
||||||
|
|
||||||
/// The list of animation layers
|
|
||||||
let layers: [LayerModel]
|
|
||||||
|
|
||||||
/// The list of glyphs used for text rendering
|
|
||||||
let glyphs: [Glyph]?
|
|
||||||
|
|
||||||
/// The list of fonts used for text rendering
|
|
||||||
let fonts: FontList?
|
|
||||||
|
|
||||||
/// Asset Library
|
|
||||||
let assetLibrary: AssetLibrary?
|
|
||||||
|
|
||||||
/// Markers
|
|
||||||
let markers: [Marker]?
|
|
||||||
let markerMap: [String : Marker]?
|
|
||||||
|
|
||||||
/// Return all marker names, in order, or an empty list if none are specified
|
|
||||||
public var markerNames: [String] {
|
|
||||||
guard let markers = markers else { return [] }
|
|
||||||
return markers.map { $0.name }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CodingKeys : String, CodingKey {
|
|
||||||
case version = "v"
|
|
||||||
case type = "ddd"
|
|
||||||
case startFrame = "ip"
|
|
||||||
case endFrame = "op"
|
|
||||||
case framerate = "fr"
|
|
||||||
case width = "w"
|
|
||||||
case height = "h"
|
|
||||||
case layers = "layers"
|
|
||||||
case glyphs = "chars"
|
|
||||||
case fonts = "fonts"
|
|
||||||
case assetLibrary = "assets"
|
|
||||||
case markers = "markers"
|
|
||||||
}
|
|
||||||
|
|
||||||
required public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: Animation.CodingKeys.self)
|
|
||||||
self.version = try container.decode(String.self, forKey: .version)
|
|
||||||
self.type = try container.decodeIfPresent(CoordinateSpace.self, forKey: .type) ?? .type2d
|
|
||||||
self.startFrame = try container.decode(AnimationFrameTime.self, forKey: .startFrame)
|
|
||||||
self.endFrame = try container.decode(AnimationFrameTime.self, forKey: .endFrame)
|
|
||||||
self.framerate = try container.decode(Double.self, forKey: .framerate)
|
|
||||||
self.width = try container.decode(Int.self, forKey: .width)
|
|
||||||
self.height = try container.decode(Int.self, forKey: .height)
|
|
||||||
self.layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
|
|
||||||
self.glyphs = try container.decodeIfPresent([Glyph].self, forKey: .glyphs)
|
|
||||||
self.fonts = try container.decodeIfPresent(FontList.self, forKey: .fonts)
|
|
||||||
self.assetLibrary = try container.decodeIfPresent(AssetLibrary.self, forKey: .assetLibrary)
|
|
||||||
self.markers = try container.decodeIfPresent([Marker].self, forKey: .markers)
|
|
||||||
|
|
||||||
if let markers = markers {
|
|
||||||
var markerMap: [String : Marker] = [:]
|
|
||||||
for marker in markers {
|
|
||||||
markerMap[marker.name] = marker
|
|
||||||
}
|
|
||||||
self.markerMap = markerMap
|
|
||||||
} else {
|
|
||||||
self.markerMap = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
//
|
|
||||||
// Asset.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/9/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public class Asset: Codable {
|
|
||||||
|
|
||||||
/// The ID of the asset
|
|
||||||
public let id: String
|
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
|
||||||
case id = "id"
|
|
||||||
}
|
|
||||||
|
|
||||||
required public init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: Asset.CodingKeys.self)
|
|
||||||
if let id = try? container.decode(String.self, forKey: .id) {
|
|
||||||
self.id = id
|
|
||||||
} else {
|
|
||||||
self.id = String(try container.decode(Int.self, forKey: .id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
//
|
|
||||||
// AssetLibrary.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/9/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
final class AssetLibrary: Codable {
|
|
||||||
|
|
||||||
/// The Assets
|
|
||||||
let assets: [String : Asset]
|
|
||||||
|
|
||||||
let imageAssets: [String : ImageAsset]
|
|
||||||
let precompAssets: [String : PrecompAsset]
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
var container = try decoder.unkeyedContainer()
|
|
||||||
var containerForKeys = container
|
|
||||||
|
|
||||||
var decodedAssets = [String : Asset]()
|
|
||||||
|
|
||||||
var imageAssets = [String : ImageAsset]()
|
|
||||||
var precompAssets = [String : PrecompAsset]()
|
|
||||||
|
|
||||||
while !container.isAtEnd {
|
|
||||||
let keyContainer = try containerForKeys.nestedContainer(keyedBy: PrecompAsset.CodingKeys.self)
|
|
||||||
if keyContainer.contains(.layers) {
|
|
||||||
let precompAsset = try container.decode(PrecompAsset.self)
|
|
||||||
decodedAssets[precompAsset.id] = precompAsset
|
|
||||||
precompAssets[precompAsset.id] = precompAsset
|
|
||||||
} else {
|
|
||||||
let imageAsset = try container.decode(ImageAsset.self)
|
|
||||||
decodedAssets[imageAsset.id] = imageAsset
|
|
||||||
imageAssets[imageAsset.id] = imageAsset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assets = decodedAssets
|
|
||||||
self.precompAssets = precompAssets
|
|
||||||
self.imageAssets = imageAssets
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.unkeyedContainer()
|
|
||||||
try container.encode(contentsOf: Array(assets.values))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
//
|
|
||||||
// ImageAsset.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/9/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public final class ImageAsset: Asset {
|
|
||||||
|
|
||||||
/// Image name
|
|
||||||
public let name: String
|
|
||||||
|
|
||||||
/// Image Directory
|
|
||||||
public let directory: String
|
|
||||||
|
|
||||||
/// Image Size
|
|
||||||
public let width: Double
|
|
||||||
|
|
||||||
public let height: Double
|
|
||||||
|
|
||||||
enum CodingKeys : String, CodingKey {
|
|
||||||
case name = "p"
|
|
||||||
case directory = "u"
|
|
||||||
case width = "w"
|
|
||||||
case height = "h"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: ImageAsset.CodingKeys.self)
|
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
|
||||||
self.directory = try container.decode(String.self, forKey: .directory)
|
|
||||||
self.width = try container.decode(Double.self, forKey: .width)
|
|
||||||
self.height = try container.decode(Double.self, forKey: .height)
|
|
||||||
try super.init(from: decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func encode(to encoder: Encoder) throws {
|
|
||||||
try super.encode(to: encoder)
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(name, forKey: .name)
|
|
||||||
try container.encode(directory, forKey: .directory)
|
|
||||||
try container.encode(width, forKey: .width)
|
|
||||||
try container.encode(height, forKey: .height)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
//
|
|
||||||
// PrecompAsset.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/9/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
final class PrecompAsset: Asset {
|
|
||||||
|
|
||||||
/// Layers of the precomp
|
|
||||||
let layers: [LayerModel]
|
|
||||||
|
|
||||||
enum CodingKeys : String, CodingKey {
|
|
||||||
case layers = "layers"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: PrecompAsset.CodingKeys.self)
|
|
||||||
self.layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
|
|
||||||
try super.init(from: decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func encode(to encoder: Encoder) throws {
|
|
||||||
try super.encode(to: encoder)
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(layers, forKey: .layers)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
|
|
||||||
extension Bundle {
|
|
||||||
func getAnimationData(_ name: String, subdirectory: String? = nil) throws -> Data? {
|
|
||||||
// Check for files in the bundle at the given path
|
|
||||||
if let url = self.url(forResource: name, withExtension: "json", subdirectory: subdirectory) {
|
|
||||||
return try Data(contentsOf: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for data assets (not available on macOS)
|
|
||||||
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
|
|
||||||
let assetKey = subdirectory != nil ? "\(subdirectory ?? "")/\(name)" : name
|
|
||||||
return NSDataAsset.init(name: assetKey, bundle: self)?.data
|
|
||||||
#else
|
|
||||||
return nil
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
// From: https://medium.com/@kewindannerfjordremeczki/swift-4-0-decodable-heterogeneous-collections-ecc0e6b468cf
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// To support a new class family, create an enum that conforms to this protocol and contains the different types.
|
|
||||||
protocol ClassFamily: Decodable {
|
|
||||||
/// The discriminator key.
|
|
||||||
static var discriminator: Discriminator { get }
|
|
||||||
|
|
||||||
/// Returns the class type of the object corresponding to the value.
|
|
||||||
func getType() -> AnyObject.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Discriminator key enum used to retrieve discriminator fields in JSON payloads.
|
|
||||||
enum Discriminator: String, CodingKey {
|
|
||||||
case type = "ty"
|
|
||||||
}
|
|
||||||
|
|
||||||
extension KeyedDecodingContainer {
|
|
||||||
|
|
||||||
/// Decode a heterogeneous list of objects for a given family.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - heterogeneousType: The decodable type of the list.
|
|
||||||
/// - family: The ClassFamily enum for the type family.
|
|
||||||
/// - key: The CodingKey to look up the list in the current container.
|
|
||||||
/// - Returns: The resulting list of heterogeneousType elements.
|
|
||||||
func decode<T : Decodable, U : ClassFamily>(_ heterogeneousType: [T].Type, ofFamily family: U.Type, forKey key: K) throws -> [T] {
|
|
||||||
var container = try self.nestedUnkeyedContainer(forKey: key)
|
|
||||||
var list = [T]()
|
|
||||||
var tmpContainer = container
|
|
||||||
while !container.isAtEnd {
|
|
||||||
let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self)
|
|
||||||
let family: U = try typeContainer.decode(U.self, forKey: U.discriminator)
|
|
||||||
if let type = family.getType() as? T.Type {
|
|
||||||
list.append(try tmpContainer.decode(type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
//
|
|
||||||
// Keyframe.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/7/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CoreGraphics
|
|
||||||
|
|
||||||
/**
|
|
||||||
Keyframe represents a point in time and is the container for datatypes.
|
|
||||||
Note: This is a parent class and should not be used directly.
|
|
||||||
*/
|
|
||||||
final class Keyframe<T: Interpolatable> {
|
|
||||||
|
|
||||||
/// The value of the keyframe
|
|
||||||
let value: T
|
|
||||||
/// The time in frames of the keyframe.
|
|
||||||
let time: CGFloat
|
|
||||||
/// A hold keyframe freezes interpolation until the next keyframe that is not a hold.
|
|
||||||
let isHold: Bool
|
|
||||||
/// The in tangent for the time interpolation curve.
|
|
||||||
let inTangent: Vector2D?
|
|
||||||
/// The out tangent for the time interpolation curve.
|
|
||||||
let outTangent: Vector2D?
|
|
||||||
|
|
||||||
/// The spacial in tangent of the vector.
|
|
||||||
let spatialInTangent: Vector3D?
|
|
||||||
/// The spacial out tangent of the vector.
|
|
||||||
let spatialOutTangent: Vector3D?
|
|
||||||
|
|
||||||
/// Initialize a value-only keyframe with no time data.
|
|
||||||
init(_ value: T,
|
|
||||||
spatialInTangent: Vector3D? = nil,
|
|
||||||
spatialOutTangent: Vector3D? = nil) {
|
|
||||||
self.value = value
|
|
||||||
self.time = 0
|
|
||||||
self.isHold = true
|
|
||||||
self.inTangent = nil
|
|
||||||
self.outTangent = nil
|
|
||||||
self.spatialInTangent = spatialInTangent
|
|
||||||
self.spatialOutTangent = spatialOutTangent
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize a keyframe
|
|
||||||
init(value: T,
|
|
||||||
time: Double,
|
|
||||||
isHold: Bool,
|
|
||||||
inTangent: Vector2D?,
|
|
||||||
outTangent: Vector2D?,
|
|
||||||
spatialInTangent: Vector3D? = nil,
|
|
||||||
spatialOutTangent: Vector3D? = nil) {
|
|
||||||
self.value = value
|
|
||||||
self.time = CGFloat(time)
|
|
||||||
self.isHold = isHold
|
|
||||||
self.outTangent = outTangent
|
|
||||||
self.inTangent = inTangent
|
|
||||||
self.spatialInTangent = spatialInTangent
|
|
||||||
self.spatialOutTangent = spatialOutTangent
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
A generic class used to parse and remap keyframe json.
|
|
||||||
|
|
||||||
Keyframe json has a couple of different variations and formats depending on the
|
|
||||||
type of keyframea and also the version of the JSON. By parsing the raw data
|
|
||||||
we can reconfigure it into a constant format.
|
|
||||||
*/
|
|
||||||
final class KeyframeData<T: Codable>: Codable {
|
|
||||||
|
|
||||||
/// The start value of the keyframe
|
|
||||||
let startValue: T?
|
|
||||||
/// The End value of the keyframe. Note: Newer versions animation json do not have this field.
|
|
||||||
let endValue: T?
|
|
||||||
/// The time in frames of the keyframe.
|
|
||||||
let time: Double?
|
|
||||||
/// A hold keyframe freezes interpolation until the next keyframe that is not a hold.
|
|
||||||
let hold: Int?
|
|
||||||
|
|
||||||
/// The in tangent for the time interpolation curve.
|
|
||||||
let inTangent: Vector2D?
|
|
||||||
/// The out tangent for the time interpolation curve.
|
|
||||||
let outTangent: Vector2D?
|
|
||||||
|
|
||||||
/// The spacial in tangent of the vector.
|
|
||||||
let spatialInTangent: Vector3D?
|
|
||||||
/// The spacial out tangent of the vector.
|
|
||||||
let spatialOutTangent:Vector3D?
|
|
||||||
|
|
||||||
init(startValue: T?,
|
|
||||||
endValue: T?,
|
|
||||||
time: Double?,
|
|
||||||
hold: Int?,
|
|
||||||
inTangent: Vector2D?,
|
|
||||||
outTangent: Vector2D?,
|
|
||||||
spatialInTangent: Vector3D?,
|
|
||||||
spatialOutTangent: Vector3D?) {
|
|
||||||
self.startValue = startValue
|
|
||||||
self.endValue = endValue
|
|
||||||
self.time = time
|
|
||||||
self.hold = hold
|
|
||||||
self.inTangent = inTangent
|
|
||||||
self.outTangent = outTangent
|
|
||||||
self.spatialInTangent = spatialInTangent
|
|
||||||
self.spatialOutTangent = spatialOutTangent
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CodingKeys : String, CodingKey {
|
|
||||||
case startValue = "s"
|
|
||||||
case endValue = "e"
|
|
||||||
case time = "t"
|
|
||||||
case hold = "h"
|
|
||||||
case inTangent = "i"
|
|
||||||
case outTangent = "o"
|
|
||||||
case spatialInTangent = "ti"
|
|
||||||
case spatialOutTangent = "to"
|
|
||||||
}
|
|
||||||
|
|
||||||
var isHold: Bool {
|
|
||||||
if let hold = hold {
|
|
||||||
return hold > 0
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
//
|
|
||||||
// KeyframeGroup.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/14/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/**
|
|
||||||
Used for coding/decoding a group of Keyframes by type.
|
|
||||||
|
|
||||||
Keyframe data is wrapped in a dictionary { "k" : KeyframeData }.
|
|
||||||
The keyframe data can either be an array of keyframes or, if no animation is present, the raw value.
|
|
||||||
This helper object is needed to properly decode the json.
|
|
||||||
*/
|
|
||||||
|
|
||||||
final class KeyframeGroup<T>: Codable where T: Codable, T: Interpolatable {
|
|
||||||
|
|
||||||
let keyframes: ContiguousArray<Keyframe<T>>
|
|
||||||
|
|
||||||
private enum KeyframeWrapperKey: String, CodingKey {
|
|
||||||
case keyframeData = "k"
|
|
||||||
}
|
|
||||||
|
|
||||||
init(keyframes: ContiguousArray<Keyframe<T>>) {
|
|
||||||
self.keyframes = keyframes
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ value: T) {
|
|
||||||
self.keyframes = [Keyframe(value)]
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: KeyframeWrapperKey.self)
|
|
||||||
|
|
||||||
if let keyframeData: T = try? container.decode(T.self, forKey: .keyframeData) {
|
|
||||||
/// Try to decode raw value; No keyframe data.
|
|
||||||
self.keyframes = [Keyframe<T>(keyframeData)]
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
Decode and array of keyframes.
|
|
||||||
|
|
||||||
Body Movin and Lottie deal with keyframes in different ways.
|
|
||||||
|
|
||||||
A keyframe object in Body movin defines a span of time with a START
|
|
||||||
and an END, from the current keyframe time to the next keyframe time.
|
|
||||||
|
|
||||||
A keyframe object in Lottie defines a singular point in time/space.
|
|
||||||
This point has an in-tangent and an out-tangent.
|
|
||||||
|
|
||||||
To properly decode this we must iterate through keyframes while holding
|
|
||||||
reference to the previous keyframe.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var keyframesContainer = try container.nestedUnkeyedContainer(forKey: .keyframeData)
|
|
||||||
var keyframes = ContiguousArray<Keyframe<T>>()
|
|
||||||
var previousKeyframeData: KeyframeData<T>?
|
|
||||||
while(!keyframesContainer.isAtEnd) {
|
|
||||||
// Ensure that Time and Value are present.
|
|
||||||
|
|
||||||
let keyframeData = try keyframesContainer.decode(KeyframeData<T>.self)
|
|
||||||
|
|
||||||
guard let value: T = keyframeData.startValue ?? previousKeyframeData?.endValue,
|
|
||||||
let time = keyframeData.time else {
|
|
||||||
/// Missing keyframe data. JSON must be corrupt.
|
|
||||||
throw DecodingError.dataCorruptedError(forKey: KeyframeWrapperKey.keyframeData, in: container, debugDescription: "Missing keyframe data.")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyframes.append(Keyframe<T>(value: value,
|
|
||||||
time: time,
|
|
||||||
isHold: keyframeData.isHold,
|
|
||||||
inTangent: previousKeyframeData?.inTangent,
|
|
||||||
outTangent: keyframeData.outTangent,
|
|
||||||
spatialInTangent: previousKeyframeData?.spatialInTangent,
|
|
||||||
spatialOutTangent: keyframeData.spatialOutTangent))
|
|
||||||
previousKeyframeData = keyframeData
|
|
||||||
}
|
|
||||||
self.keyframes = keyframes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: KeyframeWrapperKey.self)
|
|
||||||
|
|
||||||
if keyframes.count == 1 {
|
|
||||||
let keyframe = keyframes[0]
|
|
||||||
try container.encode(keyframe.value, forKey: .keyframeData)
|
|
||||||
} else {
|
|
||||||
var keyframeContainer = container.nestedUnkeyedContainer(forKey: .keyframeData)
|
|
||||||
|
|
||||||
for i in 1..<keyframes.endIndex {
|
|
||||||
let keyframe = keyframes[i-1]
|
|
||||||
let nextKeyframe = keyframes[i]
|
|
||||||
let keyframeData = KeyframeData<T>(startValue: keyframe.value,
|
|
||||||
endValue: nextKeyframe.value,
|
|
||||||
time: Double(keyframe.time),
|
|
||||||
hold: keyframe.isHold ? 1 : nil,
|
|
||||||
inTangent: nextKeyframe.inTangent,
|
|
||||||
outTangent: keyframe.outTangent,
|
|
||||||
spatialInTangent: nil,
|
|
||||||
spatialOutTangent: nil)
|
|
||||||
try keyframeContainer.encode(keyframeData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
//
|
|
||||||
// ImageLayer.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// A layer that holds an image.
|
|
||||||
final class ImageLayerModel: LayerModel {
|
|
||||||
|
|
||||||
/// The reference ID of the image.
|
|
||||||
let referenceID: String
|
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
|
||||||
case referenceID = "refId"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: ImageLayerModel.CodingKeys.self)
|
|
||||||
self.referenceID = try container.decode(String.self, forKey: .referenceID)
|
|
||||||
try super.init(from: decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func encode(to encoder: Encoder) throws {
|
|
||||||
try super.encode(to: encoder)
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(referenceID, forKey: .referenceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
//
|
|
||||||
// Layer.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/7/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// Used for mapping a heterogeneous list to classes for parsing.
|
|
||||||
extension LayerType: ClassFamily {
|
|
||||||
static var discriminator: Discriminator = .type
|
|
||||||
|
|
||||||
func getType() -> AnyObject.Type {
|
|
||||||
switch self {
|
|
||||||
case .precomp:
|
|
||||||
return PreCompLayerModel.self
|
|
||||||
case .solid:
|
|
||||||
return SolidLayerModel.self
|
|
||||||
case .image:
|
|
||||||
return ImageLayerModel.self
|
|
||||||
case .null:
|
|
||||||
return LayerModel.self
|
|
||||||
case .shape:
|
|
||||||
return ShapeLayerModel.self
|
|
||||||
case .text:
|
|
||||||
return TextLayerModel.self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum LayerType: Int, Codable {
|
|
||||||
case precomp
|
|
||||||
case solid
|
|
||||||
case image
|
|
||||||
case null
|
|
||||||
case shape
|
|
||||||
case text
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
self = try LayerType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum MatteType: Int, Codable {
|
|
||||||
case none
|
|
||||||
case add
|
|
||||||
case invert
|
|
||||||
case unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum BlendMode: Int, Codable {
|
|
||||||
case normal
|
|
||||||
case multiply
|
|
||||||
case screen
|
|
||||||
case overlay
|
|
||||||
case darken
|
|
||||||
case lighten
|
|
||||||
case colorDodge
|
|
||||||
case colorBurn
|
|
||||||
case hardLight
|
|
||||||
case softLight
|
|
||||||
case difference
|
|
||||||
case exclusion
|
|
||||||
case hue
|
|
||||||
case saturation
|
|
||||||
case color
|
|
||||||
case luminosity
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
A base top container for shapes, images, and other view objects.
|
|
||||||
*/
|
|
||||||
class LayerModel: Codable {
|
|
||||||
|
|
||||||
/// The readable name of the layer
|
|
||||||
let name: String
|
|
||||||
|
|
||||||
/// The index of the layer
|
|
||||||
let index: Int
|
|
||||||
|
|
||||||
/// The type of the layer.
|
|
||||||
let type: LayerType
|
|
||||||
|
|
||||||
/// The coordinate space
|
|
||||||
let coordinateSpace: CoordinateSpace
|
|
||||||
|
|
||||||
/// The in time of the layer in frames.
|
|
||||||
let inFrame: Double
|
|
||||||
/// The out time of the layer in frames.
|
|
||||||
let outFrame: Double
|
|
||||||
|
|
||||||
/// The start time of the layer in frames.
|
|
||||||
let startTime: Double
|
|
||||||
|
|
||||||
/// The transform of the layer
|
|
||||||
let transform: Transform
|
|
||||||
|
|
||||||
/// The index of the parent layer, if applicable.
|
|
||||||
let parent: Int?
|
|
||||||
|
|
||||||
/// The blending mode for the layer
|
|
||||||
let blendMode: BlendMode
|
|
||||||
|
|
||||||
/// An array of masks for the layer.
|
|
||||||
let masks: [Mask]?
|
|
||||||
|
|
||||||
/// A number that stretches time by a multiplier
|
|
||||||
let timeStretch: Double
|
|
||||||
|
|
||||||
/// The type of matte if any.
|
|
||||||
let matte: MatteType?
|
|
||||||
|
|
||||||
let hidden: Bool
|
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
|
||||||
case name = "nm"
|
|
||||||
case index = "ind"
|
|
||||||
case type = "ty"
|
|
||||||
case coordinateSpace = "ddd"
|
|
||||||
case inFrame = "ip"
|
|
||||||
case outFrame = "op"
|
|
||||||
case startTime = "st"
|
|
||||||
case transform = "ks"
|
|
||||||
case parent = "parent"
|
|
||||||
case blendMode = "bm"
|
|
||||||
case masks = "masksProperties"
|
|
||||||
case timeStretch = "sr"
|
|
||||||
case matte = "tt"
|
|
||||||
case hidden = "hd"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: LayerModel.CodingKeys.self)
|
|
||||||
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Layer"
|
|
||||||
self.index = try container.decode(Int.self, forKey: .index)
|
|
||||||
self.type = try container.decode(LayerType.self, forKey: .type)
|
|
||||||
self.coordinateSpace = try container.decodeIfPresent(CoordinateSpace.self, forKey: .coordinateSpace) ?? .type2d
|
|
||||||
self.inFrame = try container.decode(Double.self, forKey: .inFrame)
|
|
||||||
self.outFrame = try container.decode(Double.self, forKey: .outFrame)
|
|
||||||
self.startTime = try container.decode(Double.self, forKey: .startTime)
|
|
||||||
self.transform = try container.decode(Transform.self, forKey: .transform)
|
|
||||||
self.parent = try container.decodeIfPresent(Int.self, forKey: .parent)
|
|
||||||
self.blendMode = try container.decodeIfPresent(BlendMode.self, forKey: .blendMode) ?? .normal
|
|
||||||
self.masks = try container.decodeIfPresent([Mask].self, forKey: .masks)
|
|
||||||
self.timeStretch = try container.decodeIfPresent(Double.self, forKey: .timeStretch) ?? 1
|
|
||||||
self.matte = try container.decodeIfPresent(MatteType.self, forKey: .matte)
|
|
||||||
self.hidden = try container.decodeIfPresent(Bool.self, forKey: .hidden) ?? false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
//
|
|
||||||
// PreCompLayer.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// A layer that holds another animation composition.
|
|
||||||
final class PreCompLayerModel: LayerModel {
|
|
||||||
|
|
||||||
/// The reference ID of the precomp.
|
|
||||||
let referenceID: String
|
|
||||||
|
|
||||||
/// A value that remaps time over time.
|
|
||||||
let timeRemapping: KeyframeGroup<Vector1D>?
|
|
||||||
|
|
||||||
/// Precomp Width
|
|
||||||
let width: Double
|
|
||||||
|
|
||||||
/// Precomp Height
|
|
||||||
let height: Double
|
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
|
||||||
case referenceID = "refId"
|
|
||||||
case timeRemapping = "tm"
|
|
||||||
case width = "w"
|
|
||||||
case height = "h"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: PreCompLayerModel.CodingKeys.self)
|
|
||||||
self.referenceID = try container.decode(String.self, forKey: .referenceID)
|
|
||||||
self.timeRemapping = try container.decodeIfPresent(KeyframeGroup<Vector1D>.self, forKey: .timeRemapping)
|
|
||||||
self.width = try container.decode(Double.self, forKey: .width)
|
|
||||||
self.height = try container.decode(Double.self, forKey: .height)
|
|
||||||
try super.init(from: decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func encode(to encoder: Encoder) throws {
|
|
||||||
try super.encode(to: encoder)
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(referenceID, forKey: .referenceID)
|
|
||||||
try container.encode(timeRemapping, forKey: .timeRemapping)
|
|
||||||
try container.encode(width, forKey: .width)
|
|
||||||
try container.encode(height, forKey: .height)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
//
|
|
||||||
// ShapeLayer.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// A layer that holds vector shape objects.
|
|
||||||
final class ShapeLayerModel: LayerModel {
|
|
||||||
|
|
||||||
/// A list of shape items.
|
|
||||||
let items: [ShapeItem]
|
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
|
||||||
case items = "shapes"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: ShapeLayerModel.CodingKeys.self)
|
|
||||||
self.items = try container.decode([ShapeItem].self, ofFamily: ShapeType.self, forKey: .items)
|
|
||||||
try super.init(from: decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func encode(to encoder: Encoder) throws {
|
|
||||||
try super.encode(to: encoder)
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(self.items, forKey: .items)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
//
|
|
||||||
// SolidLayer.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// A layer that holds a solid color.
|
|
||||||
final class SolidLayerModel: LayerModel {
|
|
||||||
|
|
||||||
/// The color of the solid in Hex // Change to value provider.
|
|
||||||
let colorHex: String
|
|
||||||
|
|
||||||
/// The Width of the color layer
|
|
||||||
let width: Double
|
|
||||||
|
|
||||||
/// The height of the color layer
|
|
||||||
let height: Double
|
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
|
||||||
case colorHex = "sc"
|
|
||||||
case width = "sw"
|
|
||||||
case height = "sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: SolidLayerModel.CodingKeys.self)
|
|
||||||
self.colorHex = try container.decode(String.self, forKey: .colorHex)
|
|
||||||
self.width = try container.decode(Double.self, forKey: .width)
|
|
||||||
self.height = try container.decode(Double.self, forKey: .height)
|
|
||||||
try super.init(from: decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func encode(to encoder: Encoder) throws {
|
|
||||||
try super.encode(to: encoder)
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
try container.encode(colorHex, forKey: .colorHex)
|
|
||||||
try container.encode(width, forKey: .width)
|
|
||||||
try container.encode(height, forKey: .height)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
//
|
|
||||||
// TextLayer.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/8/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// A layer that holds text.
|
|
||||||
final class TextLayerModel: LayerModel {
|
|
||||||
|
|
||||||
/// The text for the layer
|
|
||||||
let text: KeyframeGroup<TextDocument>
|
|
||||||
|
|
||||||
/// Text animators
|
|
||||||
let animators: [TextAnimator]
|
|
||||||
|
|
||||||
private enum CodingKeys : String, CodingKey {
|
|
||||||
case textGroup = "t"
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum TextCodingKeys : String, CodingKey {
|
|
||||||
case text = "d"
|
|
||||||
case animators = "a"
|
|
||||||
}
|
|
||||||
|
|
||||||
required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: TextLayerModel.CodingKeys.self)
|
|
||||||
let textContainer = try container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup)
|
|
||||||
self.text = try textContainer.decode(KeyframeGroup<TextDocument>.self, forKey: .text)
|
|
||||||
self.animators = try textContainer.decode([TextAnimator].self, forKey: .animators)
|
|
||||||
try super.init(from: decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func encode(to encoder: Encoder) throws {
|
|
||||||
try super.encode(to: encoder)
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
var textContainer = container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup)
|
|
||||||
try textContainer.encode(text, forKey: .text)
|
|
||||||
try textContainer.encode(animators, forKey: .animators)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
//
|
|
||||||
// DashPattern.swift
|
|
||||||
// lottie-swift
|
|
||||||
//
|
|
||||||
// Created by Brandon Withrow on 1/22/19.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum DashElementType: String, Codable {
|
|
||||||
case offset = "o"
|
|
||||||
case dash = "d"
|
|
||||||
case gap = "g"
|
|
||||||
}
|
|
||||||
|
|
||||||
final class DashElement: Codable {
|
|
||||||
let type: DashElementType
|
|
||||||
let value: KeyframeGroup<Vector1D>
|
|
||||||
|
|
||||||
enum CodingKeys : String, CodingKey {
|
|
||||||
case type = "n"
|
|
||||||
case value = "v"
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user