mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35: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/TelegramCore:TelegramCore",
|
||||
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
||||
"//submodules/MeshAnimationCache:MeshAnimationCache",
|
||||
"//submodules/Utils/RangeSet:RangeSet",
|
||||
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
|
@ -10,7 +10,6 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import DeviceLocationManager
|
||||
import TemporaryCachedPeerDataManager
|
||||
import MeshAnimationCache
|
||||
import InAppPurchaseManager
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
@ -935,7 +934,6 @@ public protocol AccountContext: AnyObject {
|
||||
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
|
||||
|
||||
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
|
||||
var meshAnimationCache: MeshAnimationCache { get }
|
||||
|
||||
var animationCache: AnimationCache { get }
|
||||
var animationRenderer: MultiAnimationRenderer { get }
|
||||
|
@ -95,8 +95,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
|
||||
}
|
||||
}
|
||||
|
||||
public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? {
|
||||
func extractFileMedia(_ message: Message) -> TelegramMediaFile? {
|
||||
public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManagerPlayerType? {
|
||||
func extractFileMedia(_ message: EngineMessage) -> TelegramMediaFile? {
|
||||
var file: TelegramMediaFile?
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaFile {
|
||||
@ -120,7 +120,7 @@ public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayer
|
||||
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 {
|
||||
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
|
||||
} else if isRecentActions && !isDownloadList {
|
||||
|
@ -3,7 +3,6 @@ import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
@ -371,7 +370,7 @@ private final class DayComponent: Component {
|
||||
private var currentSelection: DaySelection?
|
||||
|
||||
private(set) var timestamp: Int32?
|
||||
private(set) var index: MessageIndex?
|
||||
private(set) var index: EngineMessage.Index?
|
||||
private var isHighlightingEnabled: Bool = false
|
||||
|
||||
init() {
|
||||
@ -983,12 +982,12 @@ public final class CalendarMessageScreen: ViewController {
|
||||
|
||||
private weak var controller: CalendarMessageScreen?
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let peerId: EnginePeer.Id
|
||||
private let initialTimestamp: Int32
|
||||
private let enableMessageRangeDeletion: Bool
|
||||
private let canNavigateToEmptyDays: Bool
|
||||
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 scrollView: Scroller
|
||||
@ -1019,13 +1018,13 @@ public final class CalendarMessageScreen: ViewController {
|
||||
init(
|
||||
controller: CalendarMessageScreen,
|
||||
context: AccountContext,
|
||||
peerId: PeerId,
|
||||
peerId: EnginePeer.Id,
|
||||
calendarSource: SparseMessageCalendar,
|
||||
initialTimestamp: Int32,
|
||||
enableMessageRangeDeletion: Bool,
|
||||
canNavigateToEmptyDays: Bool,
|
||||
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.context = context
|
||||
@ -1783,9 +1782,9 @@ public final class CalendarMessageScreen: ViewController {
|
||||
guard let calendarState = self.calendarState else {
|
||||
return
|
||||
}
|
||||
var messageMap: [Message] = []
|
||||
var messageMap: [EngineMessage] = []
|
||||
for (_, entry) in calendarState.messagesByDay {
|
||||
messageMap.append(entry.message)
|
||||
messageMap.append(EngineMessage(entry.message))
|
||||
}
|
||||
|
||||
var updatedMedia: [Int: [Int: DayMedia]] = [:]
|
||||
@ -1805,7 +1804,7 @@ public final class CalendarMessageScreen: ViewController {
|
||||
mediaLoop: for media in message.media {
|
||||
switch media {
|
||||
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
|
||||
default:
|
||||
break
|
||||
@ -1830,13 +1829,13 @@ public final class CalendarMessageScreen: ViewController {
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let peerId: EnginePeer.Id
|
||||
private let calendarSource: SparseMessageCalendar
|
||||
private let initialTimestamp: Int32
|
||||
private let enableMessageRangeDeletion: Bool
|
||||
private let canNavigateToEmptyDays: Bool
|
||||
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
|
||||
|
||||
@ -1844,13 +1843,13 @@ public final class CalendarMessageScreen: ViewController {
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
peerId: PeerId,
|
||||
peerId: EnginePeer.Id,
|
||||
calendarSource: SparseMessageCalendar,
|
||||
initialTimestamp: Int32,
|
||||
enableMessageRangeDeletion: Bool,
|
||||
canNavigateToEmptyDays: Bool,
|
||||
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.peerId = peerId
|
||||
|
@ -3,7 +3,6 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import PresentationDataUtils
|
||||
@ -17,6 +16,26 @@ import ConfettiEffect
|
||||
import TelegramUniversalVideoContent
|
||||
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 var averageProgressPerSecond: Double = 0.0
|
||||
private var lastMeasurement: (Double, Float)?
|
||||
@ -91,7 +110,7 @@ private final class ImportManager {
|
||||
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.archivePath = archivePath
|
||||
self.entries = entries
|
||||
@ -234,8 +253,8 @@ private final class ImportManager {
|
||||
|
||||
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
|
||||
|
||||
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
|
||||
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
|
||||
let unpackedFile = Signal<EngineTempBox.File, ImportError> { subscriber in
|
||||
let tempFile = EngineTempBox.shared.tempFile(fileName: entry.0.path)
|
||||
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
|
||||
let startTime = CACurrentMediaTime()
|
||||
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) {
|
||||
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)
|
||||
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 var presentationData: PresentationData
|
||||
fileprivate let cancel: () -> Void
|
||||
fileprivate var peerId: PeerId
|
||||
fileprivate var peerId: EnginePeer.Id
|
||||
private let archivePath: String?
|
||||
private let mainEntry: TempBoxFile
|
||||
private let mainEntry: EngineTempBox.File
|
||||
private let totalBytes: Int64
|
||||
private let totalMediaBytes: Int64
|
||||
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.cancel = cancel
|
||||
self.peerId = peerId
|
||||
@ -818,7 +837,7 @@ public final class ChatImportActivityScreen: ViewController {
|
||||
self.progressEstimator = ProgressEstimator()
|
||||
self.beganCompletion = false
|
||||
|
||||
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
|
||||
let resolvedPeerId: Signal<EnginePeer.Id, ImportManager.ImportError>
|
||||
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|
||||
|> mapError { _ -> ImportManager.ImportError in
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
@ -30,8 +29,8 @@ private final class ChatListFilterPresetControllerArguments {
|
||||
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
|
||||
let openAddIncludePeer: () -> Void
|
||||
let openAddExcludePeer: () -> Void
|
||||
let deleteIncludePeer: (PeerId) -> Void
|
||||
let deleteExcludePeer: (PeerId) -> Void
|
||||
let deleteIncludePeer: (EnginePeer.Id) -> Void
|
||||
let deleteExcludePeer: (EnginePeer.Id) -> Void
|
||||
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
|
||||
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
|
||||
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
|
||||
@ -49,8 +48,8 @@ private final class ChatListFilterPresetControllerArguments {
|
||||
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
|
||||
openAddIncludePeer: @escaping () -> Void,
|
||||
openAddExcludePeer: @escaping () -> Void,
|
||||
deleteIncludePeer: @escaping (PeerId) -> Void,
|
||||
deleteExcludePeer: @escaping (PeerId) -> Void,
|
||||
deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
|
||||
deleteExcludePeer: @escaping (EnginePeer.Id) -> Void,
|
||||
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
|
||||
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
|
||||
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
|
||||
@ -93,7 +92,7 @@ private enum ChatListFilterPresetControllerSection: Int32 {
|
||||
|
||||
private enum ChatListFilterPresetEntryStableId: Hashable {
|
||||
case index(Int)
|
||||
case peer(PeerId)
|
||||
case peer(EnginePeer.Id)
|
||||
case includePeerInfo
|
||||
case excludePeerInfo
|
||||
case includeCategory(ChatListFilterIncludeCategory)
|
||||
@ -311,7 +310,7 @@ private extension ChatListFilterCategoryIcon {
|
||||
}
|
||||
|
||||
private enum ChatListFilterRevealedItemId: Equatable {
|
||||
case peer(PeerId)
|
||||
case peer(EnginePeer.Id)
|
||||
case includeCategory(ChatListFilterIncludeCategory)
|
||||
case excludeCategory(ChatListFilterExcludeCategory)
|
||||
}
|
||||
@ -573,8 +572,8 @@ private struct ChatListFilterPresetControllerState: Equatable {
|
||||
var excludeMuted: Bool
|
||||
var excludeRead: Bool
|
||||
var excludeArchived: Bool
|
||||
var additionallyIncludePeers: [PeerId]
|
||||
var additionallyExcludePeers: [PeerId]
|
||||
var additionallyIncludePeers: [EnginePeer.Id]
|
||||
var additionallyExcludePeers: [EnginePeer.Id]
|
||||
|
||||
var revealedItemId: ChatListFilterRevealedItemId?
|
||||
var expandedSections: Set<FilterSection>
|
||||
@ -825,7 +824,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
return
|
||||
}
|
||||
|
||||
var includePeers: [PeerId] = []
|
||||
var includePeers: [EnginePeer.Id] = []
|
||||
for peerId in peerIds {
|
||||
switch peerId {
|
||||
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 {
|
||||
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
|
||||
var removedPeers: [PeerId] = []
|
||||
var removedPeers: [EnginePeer.Id] = []
|
||||
if let data = filter.data {
|
||||
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
|
||||
}
|
||||
@ -951,7 +950,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
|
||||
return
|
||||
}
|
||||
|
||||
var excludePeers: [PeerId] = []
|
||||
var excludePeers: [EnginePeer.Id] = []
|
||||
for peerId in peerIds {
|
||||
switch peerId {
|
||||
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)))
|
||||
}
|
||||
|
||||
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
|
||||
let currentPeers = Atomic<[EnginePeer.Id: EngineRenderedPeer]>(value: [:])
|
||||
let stateWithPeers = statePromise.get()
|
||||
|> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in
|
||||
let currentPeersValue = currentPeers.with { $0 }
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
@ -39,7 +38,7 @@ private enum ChatListFilterPresetListSection: Int32 {
|
||||
case list
|
||||
}
|
||||
|
||||
private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
|
||||
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
|
||||
if peers.isEmpty {
|
||||
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
|
||||
} else {
|
||||
|
@ -3,7 +3,6 @@ import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
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()
|
||||
case .premiumUpgrade, .premiumAnnualDiscount:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
case .chatFolderUpdates:
|
||||
nodeInteraction?.openChatFolderUpdates()
|
||||
}
|
||||
case .hide:
|
||||
switch notice {
|
||||
case .chatFolderUpdates:
|
||||
nodeInteraction?.hideChatFolderUpdates()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -966,13 +962,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
nodeInteraction?.openPasswordSetup()
|
||||
case .premiumUpgrade, .premiumAnnualDiscount:
|
||||
nodeInteraction?.openPremiumIntro()
|
||||
case .chatFolderUpdates:
|
||||
nodeInteraction?.openChatFolderUpdates()
|
||||
}
|
||||
case .hide:
|
||||
switch notice {
|
||||
case .chatFolderUpdates:
|
||||
nodeInteraction?.hideChatFolderUpdates()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -210,8 +210,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||
|
||||
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:
|
||||
strongSelf.setRevealOptions((left: [], right: []))
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import UniversalMediaPlayer
|
||||
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 {
|
||||
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
|
||||
if let value = file.duration {
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
return combineLatest(messageMediaImageStatus(context: context, messageId: message.id, image: image), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|
||||
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in
|
||||
|
@ -37,13 +37,13 @@ private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, med
|
||||
switch block {
|
||||
case let .image(id, caption, _, _):
|
||||
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
|
||||
return result
|
||||
}
|
||||
case let .video(id, caption, _, _):
|
||||
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
|
||||
return result
|
||||
}
|
||||
@ -82,7 +82,7 @@ public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galle
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -123,7 +123,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
||||
if case .suggestedProfilePhoto = action.action {
|
||||
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
|
||||
if case .photoUpdated = action.action {
|
||||
@ -131,7 +131,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
|
||||
} else {
|
||||
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)
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramBaseController
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Vision
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
@ -27,8 +26,8 @@ private final class CachedImageRecognizedContent: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId) -> Signal<CachedImageRecognizedContent?, NoError> {
|
||||
let key = ValueBoxKey(length: 20)
|
||||
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal<CachedImageRecognizedContent?, NoError> {
|
||||
let key = EngineDataBuffer(length: 20)
|
||||
key.setInt32(0, value: messageId.namespace)
|
||||
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
||||
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> {
|
||||
let key = ValueBoxKey(length: 20)
|
||||
private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> {
|
||||
let key = EngineDataBuffer(length: 20)
|
||||
key.setInt32(0, value: messageId.namespace)
|
||||
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
|
||||
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 {
|
||||
return .single([])
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
enum StickerVerificationStatus {
|
||||
@ -74,7 +73,7 @@ public class ImportStickerPack {
|
||||
let emojis: [String]
|
||||
let keywords: String
|
||||
let uuid: UUID
|
||||
var resource: MediaResource?
|
||||
var resource: EngineMediaResource?
|
||||
|
||||
init(content: Content, emojis: [String], keywords: String, uuid: UUID = UUID()) {
|
||||
self.content = content
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramUIPreferences
|
||||
@ -87,23 +86,23 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
|
||||
return
|
||||
}
|
||||
|
||||
var signals: [Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError>] = []
|
||||
var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = []
|
||||
for sticker in strongSelf.stickerPack.stickers {
|
||||
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)
|
||||
|> map { result -> (UUID, StickerVerificationStatus, MediaResource?) in
|
||||
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, EngineMediaResource?) in
|
||||
switch result {
|
||||
case .progress:
|
||||
return (sticker.uuid, .loading, nil)
|
||||
case let .complete(resource, mimeType):
|
||||
if ["application/x-tgsticker", "video/webm"].contains(mimeType) {
|
||||
return (sticker.uuid, .verified, resource)
|
||||
return (sticker.uuid, .verified, EngineMediaResource(resource))
|
||||
} else {
|
||||
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))
|
||||
})
|
||||
}
|
||||
@ -115,7 +114,7 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
|
||||
}
|
||||
var verifiedStickers = Set<UUID>()
|
||||
var declinedStickers = Set<UUID>()
|
||||
var uploadedStickerResources: [UUID: MediaResource] = [:]
|
||||
var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
|
||||
for (uuid, result, resource) in results {
|
||||
switch result {
|
||||
case .verified:
|
||||
|
@ -3,7 +3,6 @@ import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
@ -52,8 +51,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private var stickerPack: ImportStickerPack?
|
||||
var stickerResources: [UUID: MediaResource] = [:]
|
||||
private var uploadedStickerResources: [UUID: MediaResource] = [:]
|
||||
var stickerResources: [UUID: EngineMediaResource] = [:]
|
||||
private var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
|
||||
private var stickerPackReady = true
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
@ -623,11 +622,11 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
}
|
||||
if let resource = self.uploadedStickerResources[item.stickerItem.uuid] {
|
||||
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 {
|
||||
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?
|
||||
@ -695,23 +694,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
let context = strongSelf.context
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
var firstItem: 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: [])
|
||||
}
|
||||
let firstItem: StickerPackItem? = firstStickerItem?.stickerPackItem
|
||||
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 {
|
||||
(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.uploadedStickerResources = uploadedStickerResources
|
||||
var updatedItems: [StickerPackPreviewGridEntry] = []
|
||||
@ -813,8 +796,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
|
||||
} else {
|
||||
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data)
|
||||
item.resource = resource
|
||||
self.stickerResources[item.uuid] = resource
|
||||
item.resource = EngineMediaResource(resource)
|
||||
self.stickerResources[item.uuid] = EngineMediaResource(resource)
|
||||
}
|
||||
var isInitiallyVerified = false
|
||||
if case .image = item.content {
|
||||
|
@ -3,7 +3,6 @@ import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
|
@ -4,7 +4,6 @@ import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import StickerResources
|
||||
import AccountContext
|
||||
import AnimatedStickerNode
|
||||
@ -142,7 +141,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
||||
if case .video = stickerItem.content {
|
||||
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
|
||||
} else {
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import StickerResources
|
||||
@ -87,7 +86,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
||||
if case .video = item.content {
|
||||
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
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramUIPreferences
|
||||
import PersistentStringHash
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import UrlHandling
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
@ -20,11 +19,11 @@ public final class InstantPageArticleItem: InstantPageItem {
|
||||
let contentSize: CGSize
|
||||
let cover: TelegramMediaImage?
|
||||
let url: String
|
||||
let webpageId: MediaId
|
||||
let webpageId: EngineMedia.Id
|
||||
let rtl: 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.userLocation = userLocation
|
||||
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 imageSpacing: CGFloat = 10.0
|
||||
var sideInset = inset
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
@ -20,14 +19,14 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
|
||||
private var imageNode: TransformImageNode?
|
||||
|
||||
let url: String
|
||||
let webpageId: MediaId
|
||||
let webpageId: EngineMedia.Id
|
||||
let cover: TelegramMediaImage?
|
||||
|
||||
private let openUrl: (InstantPageUrlItem) -> Void
|
||||
|
||||
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.url = url
|
||||
self.webpageId = webpageId
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
@ -36,7 +35,7 @@ private func generatePauseButton(color: UIColor) -> UIImage? {
|
||||
|
||||
private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString {
|
||||
let string = NSMutableAttributedString()
|
||||
if let file = media.media as? TelegramMediaFile {
|
||||
if case let .file(file) = media.media {
|
||||
loop: for attribute in file.attributes {
|
||||
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
|
||||
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: []))
|
||||
|
||||
let playlistType: MediaManagerPlayerType
|
||||
if let file = self.media.media as? TelegramMediaFile {
|
||||
if case let .file(file) = self.media.media {
|
||||
playlistType = file.isVoice ? .voice : .music
|
||||
} else {
|
||||
playlistType = .music
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
@ -555,7 +555,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
if item is InstantPageWebEmbedItem {
|
||||
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
|
||||
}
|
||||
if item is InstantPageDetailsItem {
|
||||
@ -1003,17 +1003,17 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
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
|
||||
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 _ = 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
|
||||
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 _ = 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
|
||||
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)
|
||||
}
|
||||
})], catchTapsOutside: true)
|
||||
@ -1406,7 +1406,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
if let map = media.media as? TelegramMediaMap {
|
||||
if case let .geo(map) = media.media {
|
||||
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
|
||||
}, stopLiveLocation: { _ in
|
||||
}, openUrl: { _ in }, openPeer: { _ in
|
||||
@ -1420,12 +1420,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
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 initialIndex = 0
|
||||
for item in items {
|
||||
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 {
|
||||
initialIndex = medias.count
|
||||
}
|
||||
@ -1440,16 +1440,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var fromPlayingVideo = false
|
||||
|
||||
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))
|
||||
} else if let file = media.media as? TelegramMediaFile, file.isAnimated {
|
||||
} else if case let .file(file) = media.media, file.isAnimated {
|
||||
fromPlayingVideo = true
|
||||
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
|
||||
} else {
|
||||
fromPlayingVideo = true
|
||||
var medias: [InstantPageMedia] = mediasFromItems(items)
|
||||
medias = medias.filter {
|
||||
return $0.media is TelegramMediaImage || $0.media is TelegramMediaFile
|
||||
medias = medias.filter { item in
|
||||
switch item.media {
|
||||
case .image, .file:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for media in medias {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
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)
|
||||
} else if let file = self.media.media as? TelegramMediaFile {
|
||||
} else if case let .file(file) = self.media.media {
|
||||
if file.isVideo {
|
||||
var indexData: GalleryItemIndexData?
|
||||
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: [])
|
||||
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") {
|
||||
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
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
@ -43,7 +42,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private var fetchStatus: MediaResourceStatus?
|
||||
private var fetchStatus: EngineMediaResource.FetchStatus?
|
||||
private var fetchedDisposable = MetaDisposable()
|
||||
private var statusDisposable = MetaDisposable()
|
||||
|
||||
@ -72,7 +71,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
|
||||
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)
|
||||
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
|
||||
displayLinkDispatcher.dispatch {
|
||||
if let strongSelf = self {
|
||||
strongSelf.fetchStatus = status
|
||||
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
|
||||
strongSelf.updateFetchStatus()
|
||||
}
|
||||
}
|
||||
@ -105,7 +104,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
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)
|
||||
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) {
|
||||
@ -119,7 +118,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
|
||||
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)
|
||||
|
||||
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))
|
||||
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)
|
||||
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())
|
||||
@ -211,7 +210,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
if self.currentSize != size || self.themeUpdated {
|
||||
self.currentSize = size
|
||||
self.themeUpdated = false
|
||||
|
||||
|
||||
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.pinchContainerNode.update(size: size, transition: .immediate)
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
@ -219,7 +218,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
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)
|
||||
|
||||
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 boundingSize = size
|
||||
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()
|
||||
|
||||
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 imageSize = dimensions.cgSize.aspectFilled(size)
|
||||
let boundingSize = size
|
||||
let makeLayout = self.imageNode.asyncLayout()
|
||||
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor))
|
||||
apply()
|
||||
} else if self.media.media is TelegramMediaMap {
|
||||
} else if case .geo = self.media.media {
|
||||
for attribute in self.attributes {
|
||||
if let mapAttribute = attribute as? InstantPageMapAttribute {
|
||||
let imageSize = mapAttribute.dimensions.aspectFilled(size)
|
||||
@ -254,7 +253,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
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)
|
||||
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 boundingSize = size
|
||||
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:
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if self.media.media is TelegramMediaImage && self.media.index == -1 {
|
||||
if case .image = self.media.media, self.media.index == -1 {
|
||||
return
|
||||
}
|
||||
self.openMedia(self.media)
|
||||
@ -311,7 +310,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
|
||||
} else {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if self.media.media is TelegramMediaImage && self.media.index == -1 {
|
||||
if case .image = self.media.media, self.media.index == -1 {
|
||||
return
|
||||
}
|
||||
self.openMedia(self.media)
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
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
|
||||
var items: [InstantPageItem] = []
|
||||
@ -373,7 +372,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
contentSize.height += verticalInset
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
contentSize.height += filledSize.height
|
||||
@ -413,7 +412,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
|
||||
}
|
||||
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
|
||||
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] = []
|
||||
|
||||
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)
|
||||
} 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)
|
||||
}
|
||||
@ -460,11 +459,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
var size = CGSize()
|
||||
switch subItem {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
default:
|
||||
@ -502,9 +501,15 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
avatarInset += 62.0
|
||||
@ -572,7 +577,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
for subBlock in subItems {
|
||||
switch subBlock {
|
||||
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
|
||||
mediaIndexCounter += 1
|
||||
|
||||
@ -583,7 +588,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
if let url = url {
|
||||
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
|
||||
default:
|
||||
@ -626,11 +631,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
var contentSize: CGSize
|
||||
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
|
||||
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 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 {
|
||||
item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling)
|
||||
@ -665,7 +670,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
}
|
||||
|
||||
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)
|
||||
if offset.isZero {
|
||||
contentSize.height += 40.0
|
||||
@ -679,10 +684,10 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
if let file = media[audioId] as? TelegramMediaFile {
|
||||
if case let .file(file) = media[audioId] {
|
||||
let mediaIndex = mediaIndexCounter
|
||||
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
|
||||
items.append(item)
|
||||
@ -765,7 +770,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
|
||||
for (i, article) in articles.enumerated() {
|
||||
var cover: TelegramMediaImage?
|
||||
if let coverId = article.photoId {
|
||||
cover = media[coverId] as? TelegramMediaImage
|
||||
if case let .image(image) = media[coverId] {
|
||||
cover = image
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
contentSize.height += filledSize.height
|
||||
@ -850,12 +857,12 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLoc
|
||||
var contentSize = CGSize(width: boundingWidth, height: 0.0)
|
||||
var items: [InstantPageItem] = []
|
||||
|
||||
var media = instantPage.media
|
||||
var media = instantPage.media.mapValues(EngineMedia.init)
|
||||
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 {
|
||||
media[id] = video
|
||||
media[id] = .file(video)
|
||||
}
|
||||
|
||||
var mediaIndexCounter: Int = 0
|
||||
|
@ -1,15 +1,14 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
public struct InstantPageMedia: Equatable {
|
||||
public let index: Int
|
||||
public let media: Media
|
||||
public let media: EngineMedia
|
||||
public let url: InstantPageUrlItem?
|
||||
public let caption: 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.media = media
|
||||
self.url = url
|
||||
@ -18,6 +17,6 @@ public struct InstantPageMedia: Equatable {
|
||||
}
|
||||
|
||||
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 UIKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
@ -22,7 +21,11 @@ struct InstantPageMediaPlaylistItemId: SharedMediaPlaylistItemId {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -114,7 +117,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
|
||||
}
|
||||
|
||||
struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
|
||||
let webpageId: MediaId
|
||||
let webpageId: EngineMedia.Id
|
||||
|
||||
func isEqual(to: SharedMediaPlaylistId) -> Bool {
|
||||
if let to = to as? InstantPageMediaPlaylistId {
|
||||
@ -125,7 +128,7 @@ struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
|
||||
}
|
||||
|
||||
struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation {
|
||||
let webpageId: MediaId
|
||||
let webpageId: EngineMedia.Id
|
||||
|
||||
func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
|
||||
guard let to = to as? InstantPagePlaylistLocation else {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
@ -14,12 +13,12 @@ public final class InstantPagePeerReferenceItem: InstantPageItem {
|
||||
public let separatesTiles: Bool = false
|
||||
public let medias: [InstantPageMedia] = []
|
||||
|
||||
let initialPeer: Peer
|
||||
let initialPeer: EnginePeer
|
||||
let safeInset: CGFloat
|
||||
let transparent: 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.initialPeer = initialPeer
|
||||
self.safeInset = safeInset
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
@ -64,13 +63,13 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
private let activityIndicator: ActivityIndicator
|
||||
private let checkNode: ASImageNode
|
||||
|
||||
var peer: Peer?
|
||||
var peer: EnginePeer?
|
||||
private var peerDisposable: Disposable?
|
||||
|
||||
private let joinDisposable = MetaDisposable()
|
||||
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.strings = strings
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
@ -147,26 +146,26 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
|
||||
let account = self.context.account
|
||||
let context = self.context
|
||||
let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer)
|
||||
|> mapToSignal({ peer -> Signal<Peer, NoError> in
|
||||
let signal: Signal<EnginePeer, NoError> = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer._asPeer())
|
||||
|> mapToSignal({ peer -> Signal<EnginePeer, NoError> in
|
||||
if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil {
|
||||
return .single(peer) |> then(context.engine.peers.resolvePeerByName(name: username)
|
||||
|> mapToSignal({ updatedPeer -> Signal<Peer, NoError> in
|
||||
return .single(.channel(peer)) |> then(context.engine.peers.resolvePeerByName(name: username)
|
||||
|> mapToSignal({ updatedPeer -> Signal<EnginePeer, NoError> in
|
||||
if let updatedPeer = updatedPeer {
|
||||
return .single(updatedPeer._asPeer())
|
||||
return .single(updatedPeer)
|
||||
} else {
|
||||
return .single(peer)
|
||||
return .single(.channel(peer))
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
return .single(peer)
|
||||
return .single(EnginePeer(peer))
|
||||
}
|
||||
})
|
||||
|
||||
self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
if let strongSelf = self {
|
||||
strongSelf.peer = peer
|
||||
if let peer = peer as? TelegramChannel {
|
||||
if case let .channel(peer) = peer {
|
||||
var joinState = strongSelf.joinState
|
||||
if case .member = peer.participationStatus {
|
||||
switch joinState {
|
||||
@ -210,7 +209,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
|
||||
private func applyThemeAndStrings(themeUpdated: Bool) {
|
||||
if let peer = self.peer {
|
||||
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
|
||||
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() {
|
||||
if let peer = self.peer {
|
||||
self.openPeer(EnginePeer(peer))
|
||||
self.openPeer(peer)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
@ -29,7 +28,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
||||
|
||||
private var currentSize: CGSize?
|
||||
|
||||
private var fetchStatus: MediaResourceStatus?
|
||||
private var fetchStatus: EngineMediaResource.FetchStatus?
|
||||
private var fetchedDisposable = MetaDisposable()
|
||||
private var statusDisposable = MetaDisposable()
|
||||
|
||||
@ -47,17 +46,19 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
||||
self.openMedia = openMedia
|
||||
|
||||
var imageReference: ImageMediaReference?
|
||||
if let file = media.media as? TelegramMediaFile, let presentation = smallestImageRepresentation(file.previewRepresentations) {
|
||||
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
if case let .file(file) = media.media, let presentation = smallestImageRepresentation(file.previewRepresentations) {
|
||||
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)
|
||||
}
|
||||
|
||||
var streamVideo = false
|
||||
if let file = media.media as? TelegramMediaFile {
|
||||
var fileValue: TelegramMediaFile?
|
||||
if case let .file(file) = media.media {
|
||||
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.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
|
||||
@ -66,13 +67,13 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
|
||||
|
||||
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.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
|
||||
displayLinkDispatcher.dispatch {
|
||||
if let strongSelf = self {
|
||||
strongSelf.fetchStatus = status
|
||||
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
|
||||
strongSelf.updateFetchStatus()
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SafariServices
|
||||
import TelegramPresentationData
|
||||
@ -197,9 +196,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
|
||||
if self.contentNode == nil || self.contentNode?.frame.width != width {
|
||||
self.contentNode?.removeFromSupernode()
|
||||
|
||||
var media: [MediaId: Media] = [:]
|
||||
var media: [EngineMedia.Id: EngineMedia] = [:]
|
||||
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
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -186,9 +186,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe
|
||||
private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode {
|
||||
let media = self.items[index]
|
||||
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)
|
||||
} else if let _ = media.media as? TelegramMediaFile {
|
||||
} else if case .file = media.media {
|
||||
contentNode = ASDisplayNode()
|
||||
} else {
|
||||
contentNode = ASDisplayNode()
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramUIPreferences
|
||||
|
||||
@ -58,7 +57,7 @@ public final class InstantPageStoredState: Codable {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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> {
|
||||
let key = ValueBoxKey(length: 8)
|
||||
let key = EngineDataBuffer(length: 8)
|
||||
key.setInt64(0, value: webPage.webpageId.id)
|
||||
|
||||
if let state = state {
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
@ -2,7 +2,6 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
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 {
|
||||
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 TelegramCore
|
||||
import Display
|
||||
import Postbox
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
@ -12,9 +11,9 @@ import ContextUI
|
||||
|
||||
public final class InstantPageUrlItem: Equatable {
|
||||
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.webpageId = webpageId
|
||||
}
|
||||
@ -36,7 +35,7 @@ struct InstantPageTextStrikethroughItem {
|
||||
struct InstantPageTextImageItem {
|
||||
let frame: CGRect
|
||||
let range: NSRange
|
||||
let id: MediaId
|
||||
let id: EngineMedia.Id
|
||||
}
|
||||
|
||||
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 {
|
||||
return (nil, [], CGSize())
|
||||
}
|
||||
@ -771,7 +770,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
|
||||
extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
|
||||
}
|
||||
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 {
|
||||
let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
|
||||
for imageItem in line.imageItems {
|
||||
if let image = media[imageItem.id] as? TelegramMediaFile {
|
||||
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)
|
||||
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(image), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
|
||||
additionalItems.append(item)
|
||||
|
||||
if item.frame.minY < topInset {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AsyncDisplayKit
|
||||
import TelegramPresentationData
|
||||
|
@ -389,7 +389,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
|
||||
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
|
||||
avatarListWrapperNode.contentNode.clipsToBounds = true
|
||||
avatarListNode.backgroundColor = .clear
|
||||
avatarListNode.peer = peer
|
||||
avatarListNode.peer = EnginePeer(peer)
|
||||
avatarListNode.firstFullSizeOnly = true
|
||||
avatarListNode.offsetLocation = true
|
||||
avatarListNode.customCenterTapAction = { [weak self] in
|
||||
@ -405,7 +405,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
|
||||
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
|
||||
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.avatarListWrapperNode = avatarListWrapperNode
|
||||
|
@ -878,7 +878,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
|
||||
if statusUpdated && item.displayFileInfo {
|
||||
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
|
||||
if case .Fetching = value.fetchStatus, !item.isDownloadList {
|
||||
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||
@ -905,10 +905,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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
|
||||
if case .Fetching = value.fetchStatus, !item.isDownloadList {
|
||||
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