mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
169 lines
5.4 KiB
Swift
169 lines
5.4 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
|
|
final class BlobView: UIView {
|
|
let pointsCount: Int
|
|
let smoothness: CGFloat
|
|
|
|
let minRandomness: CGFloat
|
|
let maxRandomness: CGFloat
|
|
|
|
let minSpeed: CGFloat
|
|
let maxSpeed: CGFloat
|
|
|
|
let minScale: CGFloat
|
|
let maxScale: CGFloat
|
|
|
|
var scaleUpdated: ((CGFloat) -> Void)?
|
|
|
|
var level: CGFloat = 0 {
|
|
didSet {
|
|
if abs(self.level - oldValue) > 0.01 {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
let lv = self.minScale + (self.maxScale - self.minScale) * self.level
|
|
self.shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1)
|
|
self.scaleUpdated?(self.level)
|
|
CATransaction.commit()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var speedLevel: CGFloat = 0
|
|
private var lastSpeedLevel: CGFloat = 0
|
|
|
|
private let shapeLayer: CAShapeLayer = {
|
|
let layer = CAShapeLayer()
|
|
layer.strokeColor = nil
|
|
return layer
|
|
}()
|
|
|
|
init(
|
|
pointsCount: Int,
|
|
minRandomness: CGFloat,
|
|
maxRandomness: CGFloat,
|
|
minSpeed: CGFloat,
|
|
maxSpeed: CGFloat,
|
|
minScale: CGFloat,
|
|
maxScale: CGFloat
|
|
) {
|
|
self.pointsCount = pointsCount
|
|
self.minRandomness = minRandomness
|
|
self.maxRandomness = maxRandomness
|
|
self.minSpeed = minSpeed
|
|
self.maxSpeed = maxSpeed
|
|
self.minScale = minScale
|
|
self.maxScale = maxScale
|
|
|
|
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
|
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
|
|
|
super.init(frame: .zero)
|
|
|
|
self.layer.addSublayer(self.shapeLayer)
|
|
|
|
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func setColor(_ color: UIColor) {
|
|
self.shapeLayer.fillColor = color.cgColor
|
|
}
|
|
|
|
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
|
self.speedLevel = max(self.speedLevel, newSpeedLevel)
|
|
}
|
|
|
|
func startAnimating() {
|
|
self.animateToNewShape()
|
|
}
|
|
|
|
func stopAnimating() {
|
|
self.shapeLayer.removeAnimation(forKey: "path")
|
|
}
|
|
|
|
private func animateToNewShape() {
|
|
if self.shapeLayer.path == nil {
|
|
let points = generateNextBlob(for: self.bounds.size)
|
|
self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: smoothness).cgPath
|
|
}
|
|
|
|
let nextPoints = generateNextBlob(for: self.bounds.size)
|
|
let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: smoothness).cgPath
|
|
|
|
let animation = CABasicAnimation(keyPath: "path")
|
|
let previousPath = self.shapeLayer.path
|
|
self.shapeLayer.path = nextPath
|
|
animation.duration = CFTimeInterval(1.0 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
|
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
animation.fromValue = previousPath
|
|
animation.toValue = nextPath
|
|
animation.isRemovedOnCompletion = false
|
|
animation.fillMode = .forwards
|
|
animation.completion = { [weak self] finished in
|
|
if finished {
|
|
self?.animateToNewShape()
|
|
}
|
|
}
|
|
|
|
self.shapeLayer.add(animation, forKey: "path")
|
|
|
|
self.lastSpeedLevel = self.speedLevel
|
|
self.speedLevel = 0
|
|
}
|
|
|
|
// MARK: Helpers
|
|
|
|
private func generateNextBlob(for size: CGSize) -> [CGPoint] {
|
|
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
|
|
return blob(pointsCount: pointsCount, randomness: randomness)
|
|
.map {
|
|
return CGPoint(
|
|
x: $0.x * CGFloat(size.width),
|
|
y: $0.y * CGFloat(size.height)
|
|
)
|
|
}
|
|
}
|
|
|
|
func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
|
|
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
|
|
|
let rgen = { () -> CGFloat in
|
|
let accuracy: UInt32 = 1000
|
|
let random = arc4random_uniform(accuracy)
|
|
return CGFloat(random) / CGFloat(accuracy)
|
|
}
|
|
let rangeStart: CGFloat = 1 / (1 + randomness / 10)
|
|
|
|
let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100)
|
|
|
|
let points = (0 ..< pointsCount).map { i -> CGPoint in
|
|
let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
|
|
let angleRandomness: CGFloat = angle * 0.1
|
|
let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5)
|
|
let pointX = sin(startAngle + CGFloat(i) * randAngle)
|
|
let pointY = cos(startAngle + CGFloat(i) * randAngle)
|
|
return CGPoint(
|
|
x: pointX * randPointOffset,
|
|
y: pointY * randPointOffset
|
|
)
|
|
}
|
|
|
|
return points
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
self.shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
CATransaction.commit()
|
|
}
|
|
}
|