2024-06-12 23:04:04 +04:00

457 lines
19 KiB
Swift

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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}