import Foundation import UIKit import Display import SwiftSignalKit import TelegramCore import ComponentFlow import TelegramPresentationData import PhotoResources import AvatarNode import AccountContext final class StarsParticlesView: UIView { private struct Particle { var trackIndex: Int var position: CGPoint var scale: CGFloat var alpha: CGFloat var direction: CGPoint var velocity: CGFloat var color: UIColor var currentTime: CGFloat var lifeTime: CGFloat init( trackIndex: Int, position: CGPoint, scale: CGFloat, alpha: CGFloat, direction: CGPoint, velocity: CGFloat, color: UIColor, currentTime: CGFloat, lifeTime: CGFloat ) { self.trackIndex = trackIndex self.position = position self.scale = scale self.alpha = alpha self.direction = direction self.velocity = velocity self.color = color self.currentTime = currentTime self.lifeTime = lifeTime } mutating func update(deltaTime: CGFloat) { var position = self.position position.x += self.direction.x * self.velocity * deltaTime position.y += self.direction.y * self.velocity * deltaTime self.position = position self.currentTime += deltaTime } } private final class ParticleSet { private let size: CGSize private let large: Bool private(set) var particles: [Particle] = [] init(size: CGSize, large: Bool, preAdvance: Bool) { self.size = size self.large = large self.generateParticles(preAdvance: preAdvance) } private func generateParticles(preAdvance: Bool) { let maxDirections = self.large ? 8 : 80 if self.particles.count < maxDirections { var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections) for i in 0 ..< maxDirections { allTrackIndices[i] = i } var takenIndexCount = 0 for particle in self.particles { allTrackIndices[particle.trackIndex] = -1 takenIndexCount += 1 } var availableTrackIndices: [Int] = [] availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount) for index in allTrackIndices { if index != -1 { availableTrackIndices.append(index) } } if !availableTrackIndices.isEmpty { availableTrackIndices.shuffle() for takeIndex in availableTrackIndices { let directionIndex = takeIndex var angle = (CGFloat(directionIndex % maxDirections) / CGFloat(maxDirections)) * CGFloat.pi * 2.0 var alpha = 1.0 var lifeTimeMultiplier = 1.0 var isUpOrDownSemisphere = false if angle > CGFloat.pi / 7.0 && angle < CGFloat.pi - CGFloat.pi / 7.0 { isUpOrDownSemisphere = true } else if !"".isEmpty, angle > CGFloat.pi + CGFloat.pi / 7.0 && angle < 2.0 * CGFloat.pi - CGFloat.pi / 7.0 { isUpOrDownSemisphere = true } if isUpOrDownSemisphere { if CGFloat.random(in: 0.0 ... 1.0) < 0.2 { lifeTimeMultiplier = 0.3 } else { angle += CGFloat.random(in: 0.0 ... 1.0) > 0.5 ? CGFloat.pi / 1.6 : -CGFloat.pi / 1.6 angle += CGFloat.random(in: -0.2 ... 0.2) lifeTimeMultiplier = 0.5 } if self.large { alpha = 0.0 } } if self.large { angle += CGFloat.random(in: -0.5 ... 0.5) } let direction = CGPoint(x: cos(angle), y: sin(angle)) let velocity = self.large ? CGFloat.random(in: 15.0 ..< 20.0) : CGFloat.random(in: 20.0 ..< 35.0) let scale = self.large ? CGFloat.random(in: 0.65 ... 0.9) : CGFloat.random(in: 0.65 ... 1.0) * 0.75 let lifeTime = (self.large ? CGFloat.random(in: 2.0 ... 3.5) : CGFloat.random(in: 0.7 ... 3.0)) var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0) var initialOffset: CGFloat = 0.5 if preAdvance { initialOffset = CGFloat.random(in: 0.5 ... 1.0) } else { let p = CGFloat.random(in: 0.0 ... 1.0) if p < 0.5 { initialOffset = CGFloat.random(in: 0.65 ... 1.0) } else { initialOffset = 0.5 } } position.x += direction.x * initialOffset * 105.0 position.y += direction.y * initialOffset * 105.0 let largeColors: [UInt32] = [0xff9145, 0xfec007, 0xed9303] let smallColors: [UInt32] = [0xfecc14, 0xf7ab04, 0xff9145, 0xfdda21] let particle = Particle( trackIndex: directionIndex, position: position, scale: scale, alpha: alpha, direction: direction, velocity: velocity, color: UIColor(rgb: (self.large ? largeColors : smallColors).randomElement()!), currentTime: 0.0, lifeTime: lifeTime * lifeTimeMultiplier ) self.particles.append(particle) } } } } func update(deltaTime: CGFloat) { for i in (0 ..< self.particles.count).reversed() { self.particles[i].update(deltaTime: deltaTime) if self.particles[i].currentTime > self.particles[i].lifeTime { self.particles.remove(at: i) } } self.generateParticles(preAdvance: false) } } private var displayLink: SharedDisplayLinkDriver.Link? private var particleSet: ParticleSet? private let particleImage: UIImage private var particleLayers: [SimpleLayer] = [] private var size: CGSize? private let large: Bool init(size: CGSize, large: Bool) { if large { self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/PremiumIcon"), color: .white)!.withRenderingMode(.alwaysTemplate) } else { self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Particle"), color: .white)!.withRenderingMode(.alwaysTemplate) } self.large = large super.init(frame: .zero) self.particleSet = ParticleSet(size: size, large: large, preAdvance: true) self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in self?.update(deltaTime: CGFloat(delta)) }) } required init?(coder: NSCoder) { preconditionFailure() } fileprivate func update(size: CGSize) { self.size = size } private func update(deltaTime: CGFloat) { guard let particleSet = self.particleSet else { return } particleSet.update(deltaTime: deltaTime) for i in 0 ..< particleSet.particles.count { let particle = particleSet.particles[i] let particleLayer: SimpleLayer if i < self.particleLayers.count { particleLayer = self.particleLayers[i] particleLayer.isHidden = false } else { particleLayer = SimpleLayer() particleLayer.contents = self.particleImage.cgImage particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size) self.particleLayers.append(particleLayer) self.layer.addSublayer(particleLayer) } particleLayer.layerTintColor = particle.color.cgColor particleLayer.position = particle.position particleLayer.opacity = Float(particle.alpha) let particleScale = min(1.0, particle.currentTime / 0.3) * min(1.0, (particle.lifeTime - particle.currentTime) / 0.2) * particle.scale particleLayer.transform = CATransform3DMakeScale(particleScale, particleScale, 1.0) } if particleSet.particles.count < self.particleLayers.count { for i in particleSet.particles.count ..< self.particleLayers.count { self.particleLayers[i].isHidden = true } } } } public final class StarsImageComponent: Component { public enum Subject: Equatable { case none case photo(TelegramMediaWebFile) case transactionPeer(StarsContext.State.Transaction.Peer) } public let context: AccountContext public let subject: Subject public let theme: PresentationTheme public let diameter: CGFloat public init( context: AccountContext, subject: Subject, theme: PresentationTheme, diameter: CGFloat ) { self.context = context self.subject = subject self.theme = theme self.diameter = diameter } public static func ==(lhs: StarsImageComponent, rhs: StarsImageComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.subject != rhs.subject { return false } if lhs.diameter != rhs.diameter { return false } return true } public final class View: UIView { private var component: StarsImageComponent? private var smallParticlesView: StarsParticlesView? private var largeParticlesView: StarsParticlesView? private var imageNode: TransformImageNode? private var avatarNode: ImageNode? private var iconBackgroundView: UIImageView? private var iconView: UIImageView? private let fetchDisposable = MetaDisposable() public override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { preconditionFailure() } deinit { self.fetchDisposable.dispose() } func update(component: StarsImageComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component let smallParticlesView: StarsParticlesView if let current = self.smallParticlesView { smallParticlesView = current } else { smallParticlesView = StarsParticlesView(size: availableSize, large: false) self.addSubview(smallParticlesView) self.smallParticlesView = smallParticlesView } smallParticlesView.update(size: availableSize) smallParticlesView.frame = CGRect(origin: .zero, size: availableSize) let largeParticlesView: StarsParticlesView if let current = self.largeParticlesView { largeParticlesView = current } else { largeParticlesView = StarsParticlesView(size: availableSize, large: true) self.addSubview(largeParticlesView) self.largeParticlesView = largeParticlesView } largeParticlesView.update(size: availableSize) largeParticlesView.frame = CGRect(origin: .zero, size: availableSize) let imageSize = CGSize(width: component.diameter, height: component.diameter) let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - imageSize.height) / 2.0)), size: imageSize) switch component.subject { case .none: break case let .photo(photo): let imageNode: TransformImageNode if let current = self.imageNode { imageNode = current } else { imageNode = TransformImageNode() imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] self.addSubview(imageNode.view) self.imageNode = imageNode imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo)) self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict()) } imageNode.frame = imageFrame imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() case let .transactionPeer(peer): if case let .peer(peer) = peer { let avatarNode: ImageNode if let current = self.avatarNode { avatarNode = current } else { avatarNode = ImageNode() avatarNode.displaysAsynchronously = false self.addSubview(avatarNode.view) self.avatarNode = avatarNode avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: imageSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) } avatarNode.frame = imageFrame } else { let iconBackgroundView: UIImageView let iconView: UIImageView if let currentBackground = self.iconBackgroundView, let current = self.iconView { iconBackgroundView = currentBackground iconView = current } else { iconBackgroundView = UIImageView() iconView = UIImageView() self.addSubview(iconBackgroundView) self.addSubview(iconView) self.iconBackgroundView = iconBackgroundView self.iconView = iconView } var iconInset: CGFloat = 9.0 var iconOffset: CGFloat = 0.0 switch peer { case .appStore: iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor ], direction: .mirroredDiagonal ) iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple") case .playMarket: iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor ], direction: .mirroredDiagonal ) iconView.image = UIImage(bundleImageName: "Premium/Stars/Google") case .fragment: iconBackgroundView.image = generateFilledCircleImage( diameter: imageSize.width, color: UIColor(rgb: 0x1b1f24) ) iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") iconOffset = 5.0 case .premiumBot: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0x6b93ff).cgColor, UIColor(rgb: 0x6b93ff).cgColor, UIColor(rgb: 0x8d77ff).cgColor, UIColor(rgb: 0xb56eec).cgColor, UIColor(rgb: 0xb56eec).cgColor ], direction: .mirroredDiagonal ) iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) case .peer, .unsupported: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0xb1b1b1).cgColor, UIColor(rgb: 0xcdcdcd).cgColor ], direction: .mirroredDiagonal ) iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) } iconBackgroundView.frame = imageFrame iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) } } return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } }