Swiftgram/submodules/PremiumUI/Sources/TodoChecksView.swift
2025-06-20 23:01:21 +02:00

268 lines
10 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import CheckNode
final class TodoChecksView: UIView, PhoneDemoDecorationView {
private struct Particle {
var id: Int64
var trackIndex: Int
var position: CGPoint
var scale: CGFloat
var alpha: CGFloat
var direction: CGPoint
var velocity: CGFloat
var rotation: CGFloat
var currentTime: CGFloat
var lifeTime: CGFloat
var checkTime: CGFloat?
var didSetup: Bool = false
init(
trackIndex: Int,
position: CGPoint,
scale: CGFloat,
alpha: CGFloat,
direction: CGPoint,
velocity: CGFloat,
rotation: CGFloat,
currentTime: CGFloat,
lifeTime: CGFloat,
checkTime: CGFloat?
) {
self.id = Int64.random(in: 0 ..< .max)
self.trackIndex = trackIndex
self.position = position
self.scale = scale
self.alpha = alpha
self.direction = direction
self.velocity = velocity
self.rotation = rotation
self.currentTime = currentTime
self.lifeTime = lifeTime
self.checkTime = checkTime
}
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
}
mutating func setup() {
self.didSetup = true
}
}
private final class ParticleSet {
private let size: CGSize
var particles: [Particle] = []
init(size: CGSize, preAdvance: Bool) {
self.size = size
self.generateParticles(preAdvance: preAdvance)
}
private func generateParticles(preAdvance: Bool) {
let maxDirections = 16
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
let angle: CGFloat
if directionIndex < 8 {
angle = (CGFloat(directionIndex) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.0)
} else {
angle = CGFloat.pi + (CGFloat(directionIndex - 6) / 5.0 - 0.5) * 2.0 * (CGFloat.pi / 4.0)
}
let lifeTimeMultiplier = 1.0
let scale = 1.0
let direction = CGPoint(x: cos(angle), y: sin(angle))
let velocity = CGFloat.random(in: 18.0 ..< 22.0)
let lifeTime = CGFloat.random(in: 3.2 ... 4.2)
var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0 + 40.0)
var initialOffset: CGFloat = 0.5
if preAdvance {
initialOffset = CGFloat.random(in: 0.7 ... 0.7)
} else {
initialOffset = CGFloat.random(in: 0.60 ... 0.72)
}
position.x += direction.x * initialOffset * 250.0
position.y += direction.y * initialOffset * 330.0
var checkTime: CGFloat?
let p = CGFloat.random(in: 0.0 ... 1.0)
if p < 0.2 {
checkTime = 0.0
} else if p < 0.6 {
checkTime = 1.2 + CGFloat.random(in: 0.1 ... 0.6)
}
let particle = Particle(
trackIndex: directionIndex,
position: position,
scale: scale,
alpha: 0.3,
direction: direction,
velocity: velocity,
rotation: CGFloat.random(in: -0.18 ... 0.2),
currentTime: 0.0,
lifeTime: lifeTime * lifeTimeMultiplier,
checkTime: checkTime
)
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 var particleLayers: [CheckLayer] = []
private var particleMap: [Int64: CheckLayer] = [:]
private var size: CGSize?
private let large: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
self.particleSet = ParticleSet(size: frame.size, preAdvance: false)
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)
var validIds = Set<Int64>()
for i in 0 ..< particleSet.particles.count {
validIds.insert(particleSet.particles[i].id)
}
for id in self.particleMap.keys {
if !validIds.contains(id) {
self.particleMap[id]?.isHidden = true
self.particleMap.removeValue(forKey: id)
}
}
for i in 0 ..< particleSet.particles.count {
let particle = particleSet.particles[i]
let particleLayer: CheckLayer
if let assignedLayer = self.particleMap[particle.id] {
particleLayer = assignedLayer
} else {
if i < self.particleLayers.count, let availableLayer = self.particleLayers.first(where: { $0.isHidden }) {
particleLayer = availableLayer
particleLayer.isHidden = false
} else {
particleLayer = CheckLayer()
particleLayer.animateScale = false
particleLayer.theme = CheckNodeTheme(backgroundColor: .white, strokeColor: .clear, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false)
particleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 22.0, height: 22.0))
self.particleLayers.append(particleLayer)
self.layer.addSublayer(particleLayer)
}
self.particleMap[particle.id] = particleLayer
}
if !particle.didSetup {
particleLayer.setSelected(false, animated: false)
particleSet.particles[i].setup()
}
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
var transform = CATransform3DMakeScale(particleScale, particleScale, 1.0)
transform = CATransform3DRotate(transform, particle.rotation, 0.0, 0.0, 1.0)
particleLayer.transform = transform
if let checkTime = particle.checkTime, particle.currentTime >= checkTime, !particleLayer.selected {
particleLayer.setSelected(true, animated: true)
}
}
}
private var visible = false
func setVisible(_ visible: Bool) {
guard self.visible != visible else {
return
}
self.visible = visible
self.displayLink?.isPaused = !visible
// let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
// transition.updateAlpha(layer: self.containerView.layer, alpha: visible ? 1.0 : 0.0, completion: { [weak self] finished in
// if let strongSelf = self, finished && !visible && !strongSelf.visible {
// for view in strongSelf.containerView.subviews {
// view.removeFromSuperview()
// }
// }
// })
}
func resetAnimation() {
}
}