mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
425 lines
18 KiB
Swift
425 lines
18 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import SwiftSignalKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import AppBundle
|
|
import LegacyComponents
|
|
|
|
public class MediaDustLayer: CALayer {
|
|
private var emitter: CAEmitterCell?
|
|
private var emitterLayer: CAEmitterLayer?
|
|
|
|
private var size: CGSize?
|
|
|
|
override public init() {
|
|
super.init()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func setupEmitterLayerIfNeeded() {
|
|
guard self.emitterLayer == nil else {
|
|
return
|
|
}
|
|
|
|
let emitter = CAEmitterCell()
|
|
emitter.color = UIColor(rgb: 0xffffff, alpha: 0.0).cgColor
|
|
emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage
|
|
emitter.contentsScale = 1.8
|
|
emitter.emissionRange = .pi * 2.0
|
|
emitter.lifetime = 8.0
|
|
emitter.scale = 0.5
|
|
emitter.velocityRange = 0.0
|
|
emitter.name = "dustCell"
|
|
emitter.alphaRange = 1.0
|
|
emitter.setValue("point", forKey: "particleType")
|
|
emitter.setValue(1.0, forKey: "mass")
|
|
emitter.setValue(0.01, forKey: "massRange")
|
|
self.emitter = emitter
|
|
|
|
let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
|
|
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
|
|
alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values")
|
|
alphaBehavior.setValue(true, forKey: "additive")
|
|
|
|
let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
|
|
scaleBehavior.setValue("scale", forKey: "keyPath")
|
|
scaleBehavior.setValue([0.0, 0.5], forKey: "values")
|
|
scaleBehavior.setValue([0.0, 0.05], forKey: "locations")
|
|
|
|
let behaviors = [alphaBehavior, scaleBehavior]
|
|
|
|
let emitterLayer = CAEmitterLayer()
|
|
emitterLayer.masksToBounds = true
|
|
emitterLayer.allowsGroupOpacity = true
|
|
emitterLayer.lifetime = 1
|
|
emitterLayer.emitterCells = [emitter]
|
|
emitterLayer.seed = arc4random()
|
|
emitterLayer.emitterShape = .rectangle
|
|
emitterLayer.setValue(behaviors, forKey: "emitterBehaviors")
|
|
self.addSublayer(emitterLayer)
|
|
|
|
self.emitterLayer = emitterLayer
|
|
}
|
|
|
|
private func updateEmitter() {
|
|
guard let size = self.size else {
|
|
return
|
|
}
|
|
|
|
self.setupEmitterLayerIfNeeded()
|
|
|
|
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
|
|
self.emitterLayer?.emitterSize = size
|
|
self.emitterLayer?.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
|
|
|
let square = Float(size.width * size.height)
|
|
Queue.mainQueue().async {
|
|
self.emitter?.birthRate = min(100000.0, square * 0.02)
|
|
}
|
|
}
|
|
|
|
public func updateLayout(size: CGSize) {
|
|
self.size = size
|
|
|
|
self.updateEmitter()
|
|
}
|
|
}
|
|
|
|
public class MediaDustNode: ASDisplayNode {
|
|
private var currentParams: (size: CGSize, color: UIColor)?
|
|
private var animColor: CGColor?
|
|
private let enableAnimations: Bool
|
|
|
|
private var emitterNode: ASDisplayNode
|
|
private var emitter: CAEmitterCell?
|
|
private var emitterLayer: CAEmitterLayer?
|
|
|
|
private let emitterMaskNode: ASDisplayNode
|
|
private let emitterSpotNode: ASImageNode
|
|
private let emitterMaskFillNode: ASDisplayNode
|
|
|
|
private var staticNode: ASImageNode?
|
|
private var staticParams: CGSize?
|
|
|
|
public var isRevealed = false
|
|
private var isExploding = false
|
|
|
|
public var revealed: () -> Void = {}
|
|
public var tapped: () -> Void = {}
|
|
|
|
public init(enableAnimations: Bool) {
|
|
self.enableAnimations = enableAnimations
|
|
|
|
self.emitterNode = ASDisplayNode()
|
|
self.emitterNode.isUserInteractionEnabled = false
|
|
self.emitterNode.clipsToBounds = true
|
|
|
|
self.emitterMaskNode = ASDisplayNode()
|
|
self.emitterSpotNode = ASImageNode()
|
|
self.emitterSpotNode.contentMode = .scaleToFill
|
|
self.emitterSpotNode.isUserInteractionEnabled = false
|
|
|
|
self.emitterMaskFillNode = ASDisplayNode()
|
|
self.emitterMaskFillNode.backgroundColor = .white
|
|
self.emitterMaskFillNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.emitterNode)
|
|
|
|
self.emitterMaskNode.addSubnode(self.emitterSpotNode)
|
|
self.emitterMaskNode.addSubnode(self.emitterMaskFillNode)
|
|
}
|
|
|
|
public override func didLoad() {
|
|
super.didLoad()
|
|
|
|
if self.enableAnimations {
|
|
let emitter = CAEmitterCell()
|
|
emitter.color = UIColor(rgb: 0xffffff, alpha: 0.0).cgColor
|
|
emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage
|
|
emitter.contentsScale = 1.8
|
|
emitter.emissionRange = .pi * 2.0
|
|
emitter.lifetime = 8.0
|
|
emitter.scale = 0.5
|
|
emitter.velocityRange = 0.0
|
|
emitter.name = "dustCell"
|
|
emitter.alphaRange = 1.0
|
|
emitter.setValue("point", forKey: "particleType")
|
|
emitter.setValue(1.0, forKey: "mass")
|
|
emitter.setValue(0.01, forKey: "massRange")
|
|
self.emitter = emitter
|
|
|
|
let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
|
|
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
|
|
alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values")
|
|
alphaBehavior.setValue(true, forKey: "additive")
|
|
|
|
let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
|
|
scaleBehavior.setValue("scale", forKey: "keyPath")
|
|
scaleBehavior.setValue([0.0, 0.5], forKey: "values")
|
|
scaleBehavior.setValue([0.0, 0.05], forKey: "locations")
|
|
|
|
let randomAttractor0 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
|
|
randomAttractor0.setValue("randomAttractor0", forKey: "name")
|
|
randomAttractor0.setValue(20, forKey: "falloff")
|
|
randomAttractor0.setValue(35, forKey: "radius")
|
|
randomAttractor0.setValue(5, forKey: "stiffness")
|
|
randomAttractor0.setValue(NSValue(cgPoint: .zero), forKey: "position")
|
|
|
|
let randomAttractor1 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
|
|
randomAttractor1.setValue("randomAttractor1", forKey: "name")
|
|
randomAttractor1.setValue(20, forKey: "falloff")
|
|
randomAttractor1.setValue(35, forKey: "radius")
|
|
randomAttractor1.setValue(5, forKey: "stiffness")
|
|
randomAttractor1.setValue(NSValue(cgPoint: .zero), forKey: "position")
|
|
|
|
let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
|
|
fingerAttractor.setValue("fingerAttractor", forKey: "name")
|
|
|
|
let behaviors = [randomAttractor0, randomAttractor1, fingerAttractor, alphaBehavior, scaleBehavior]
|
|
|
|
let emitterLayer = CAEmitterLayer()
|
|
emitterLayer.masksToBounds = true
|
|
emitterLayer.allowsGroupOpacity = true
|
|
emitterLayer.lifetime = 1
|
|
emitterLayer.emitterCells = [emitter]
|
|
emitterLayer.seed = arc4random()
|
|
emitterLayer.emitterShape = .rectangle
|
|
emitterLayer.setValue(behaviors, forKey: "emitterBehaviors")
|
|
|
|
emitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.fingerAttractor.stiffness")
|
|
emitterLayer.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
|
|
|
self.emitterLayer = emitterLayer
|
|
|
|
self.emitterNode.layer.addSublayer(emitterLayer)
|
|
} else {
|
|
let staticNode = ASImageNode()
|
|
self.staticNode = staticNode
|
|
self.addSubnode(staticNode)
|
|
}
|
|
|
|
self.updateEmitter()
|
|
|
|
self.setupRandomAnimations()
|
|
|
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:))))
|
|
}
|
|
|
|
public func tap(at location: CGPoint) {
|
|
guard !self.isRevealed else {
|
|
return
|
|
}
|
|
|
|
self.tapped()
|
|
|
|
self.isRevealed = true
|
|
|
|
if self.enableAnimations {
|
|
self.isExploding = true
|
|
|
|
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
|
self.emitterLayer?.setValue(location, forKeyPath: "emitterBehaviors.fingerAttractor.position")
|
|
|
|
let maskSize = self.emitterNode.frame.size
|
|
Queue.concurrentDefaultQueue().async {
|
|
let emitterMaskImage = generateMaskImage(size: maskSize, position: location, inverse: true)
|
|
|
|
Queue.mainQueue().async {
|
|
self.emitterSpotNode.image = emitterMaskImage
|
|
}
|
|
}
|
|
|
|
Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) {
|
|
let xFactor = (location.x / self.emitterNode.frame.width - 0.5) * 2.0
|
|
let yFactor = (location.y / self.emitterNode.frame.height - 0.5) * 2.0
|
|
let maxFactor = max(abs(xFactor), abs(yFactor))
|
|
|
|
let scaleAddition = maxFactor * 4.0
|
|
let durationAddition = -maxFactor * 0.2
|
|
|
|
self.supernode?.view.mask = self.emitterMaskNode.view
|
|
self.emitterSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0)
|
|
|
|
self.emitterSpotNode.layer.anchorPoint = CGPoint(x: location.x / self.emitterMaskNode.frame.width, y: location.y / self.emitterMaskNode.frame.height)
|
|
self.emitterSpotNode.position = location
|
|
self.emitterSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.45 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in
|
|
self?.revealed()
|
|
self?.alpha = 0.0
|
|
self?.supernode?.view.mask = nil
|
|
|
|
})
|
|
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
|
|
self.isExploding = false
|
|
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
|
|
|
|
self.emitterSpotNode.layer.removeAllAnimations()
|
|
self.emitterMaskFillNode.layer.removeAllAnimations()
|
|
}
|
|
} else {
|
|
self.supernode?.alpha = 0.0
|
|
self.supernode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, completion: { [weak self] _ in
|
|
self?.revealed()
|
|
})
|
|
}
|
|
}
|
|
|
|
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
let location = gestureRecognizer.location(in: self.view)
|
|
self.tap(at: location)
|
|
}
|
|
|
|
private var didSetupAnimations = false
|
|
private func setupRandomAnimations() {
|
|
guard self.frame.width > 0.0, self.emitterLayer != nil, !self.didSetupAnimations else {
|
|
return
|
|
}
|
|
self.didSetupAnimations = true
|
|
|
|
let falloffAnimation1 = CABasicAnimation(keyPath: "emitterBehaviors.randomAttractor0.falloff")
|
|
falloffAnimation1.beginTime = 0.0
|
|
falloffAnimation1.fillMode = .both
|
|
falloffAnimation1.isRemovedOnCompletion = false
|
|
falloffAnimation1.autoreverses = true
|
|
falloffAnimation1.repeatCount = .infinity
|
|
falloffAnimation1.duration = 2.0
|
|
falloffAnimation1.fromValue = -20.0 as NSNumber
|
|
falloffAnimation1.toValue = 60.0 as NSNumber
|
|
falloffAnimation1.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
self.emitterLayer?.add(falloffAnimation1, forKey: "emitterBehaviors.randomAttractor0.falloff")
|
|
|
|
let positionAnimation1 = CAKeyframeAnimation(keyPath: "emitterBehaviors.randomAttractor0.position")
|
|
positionAnimation1.beginTime = 0.0
|
|
positionAnimation1.fillMode = .both
|
|
positionAnimation1.isRemovedOnCompletion = false
|
|
positionAnimation1.autoreverses = true
|
|
positionAnimation1.repeatCount = .infinity
|
|
positionAnimation1.duration = 3.0
|
|
positionAnimation1.calculationMode = .discrete
|
|
|
|
let xInset1: CGFloat = self.frame.width * 0.2
|
|
let yInset1: CGFloat = self.frame.height * 0.2
|
|
var positionValues1: [CGPoint] = []
|
|
for _ in 0 ..< 35 {
|
|
positionValues1.append(CGPoint(x: CGFloat.random(in: xInset1 ..< self.frame.width - xInset1), y: CGFloat.random(in: yInset1 ..< self.frame.height - yInset1)))
|
|
}
|
|
positionAnimation1.values = positionValues1
|
|
|
|
self.emitterLayer?.add(positionAnimation1, forKey: "emitterBehaviors.randomAttractor0.position")
|
|
|
|
let falloffAnimation2 = CABasicAnimation(keyPath: "emitterBehaviors.randomAttractor1.falloff")
|
|
falloffAnimation2.beginTime = 0.0
|
|
falloffAnimation2.fillMode = .both
|
|
falloffAnimation2.isRemovedOnCompletion = false
|
|
falloffAnimation2.autoreverses = true
|
|
falloffAnimation2.repeatCount = .infinity
|
|
falloffAnimation2.duration = 2.0
|
|
falloffAnimation2.fromValue = -20.0 as NSNumber
|
|
falloffAnimation2.toValue = 60.0 as NSNumber
|
|
falloffAnimation2.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
self.emitterLayer?.add(falloffAnimation2, forKey: "emitterBehaviors.randomAttractor1.falloff")
|
|
|
|
let positionAnimation2 = CAKeyframeAnimation(keyPath: "emitterBehaviors.randomAttractor1.position")
|
|
positionAnimation2.beginTime = 0.0
|
|
positionAnimation2.fillMode = .both
|
|
positionAnimation2.isRemovedOnCompletion = false
|
|
positionAnimation2.autoreverses = true
|
|
positionAnimation2.repeatCount = .infinity
|
|
positionAnimation2.duration = 3.0
|
|
positionAnimation2.calculationMode = .discrete
|
|
|
|
let xInset2: CGFloat = self.frame.width * 0.1
|
|
let yInset2: CGFloat = self.frame.height * 0.1
|
|
var positionValues2: [CGPoint] = []
|
|
for _ in 0 ..< 35 {
|
|
positionValues2.append(CGPoint(x: CGFloat.random(in: xInset2 ..< self.frame.width - xInset2), y: CGFloat.random(in: yInset2 ..< self.frame.height - yInset2)))
|
|
}
|
|
positionAnimation2.values = positionValues2
|
|
|
|
self.emitterLayer?.add(positionAnimation2, forKey: "emitterBehaviors.randomAttractor1.position")
|
|
}
|
|
|
|
private func updateEmitter() {
|
|
guard let (size, _) = self.currentParams else {
|
|
return
|
|
}
|
|
if self.enableAnimations {
|
|
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
|
|
self.emitterLayer?.emitterSize = size
|
|
self.emitterLayer?.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
|
|
|
let radius = max(size.width, size.height)
|
|
self.emitterLayer?.setValue(max(size.width, size.height), forKeyPath: "emitterBehaviors.fingerAttractor.radius")
|
|
self.emitterLayer?.setValue(radius * -0.5, forKeyPath: "emitterBehaviors.fingerAttractor.falloff")
|
|
|
|
let square = Float(size.width * size.height)
|
|
Queue.mainQueue().async {
|
|
self.emitter?.birthRate = min(100000.0, square * 0.02)
|
|
}
|
|
} else {
|
|
if let staticParams = self.staticParams, staticParams == size && self.staticNode?.image != nil {
|
|
return
|
|
}
|
|
self.staticParams = size
|
|
|
|
let start = CACurrentMediaTime()
|
|
|
|
Queue.concurrentDefaultQueue().async {
|
|
var generator = ArbitraryRandomNumberGenerator(seed: 1)
|
|
let image = generateImage(size, rotatedContext: { size, context in
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
context.clear(bounds)
|
|
|
|
context.setFillColor(UIColor.white.cgColor)
|
|
let rect = CGRect(origin: .zero, size: size)
|
|
|
|
let rate = Int(rect.width * rect.height * 0.04)
|
|
for _ in 0 ..< rate {
|
|
let location = CGPoint(x: .random(in: rect.minX ..< rect.maxX, using: &generator), y: .random(in: rect.minY ..< rect.maxY, using: &generator))
|
|
context.fillEllipse(in: CGRect(origin: location, size: CGSize(width: 1.0, height: 1.0)))
|
|
}
|
|
})
|
|
Queue.mainQueue().async {
|
|
self.staticNode?.image = image
|
|
}
|
|
}
|
|
self.staticNode?.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
print("total draw \(CACurrentMediaTime() - start)")
|
|
}
|
|
}
|
|
|
|
public func update(size: CGSize, color: UIColor, transition: ContainedViewLayoutTransition) {
|
|
self.currentParams = (size, color)
|
|
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
transition.updateFrame(node: self.emitterNode, frame: bounds)
|
|
|
|
self.emitterMaskNode.frame = bounds
|
|
self.emitterMaskFillNode.frame = bounds
|
|
|
|
self.staticNode?.frame = bounds
|
|
|
|
if self.isNodeLoaded {
|
|
self.updateEmitter()
|
|
self.setupRandomAnimations()
|
|
}
|
|
}
|
|
|
|
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
if !self.isRevealed {
|
|
return super.point(inside: point, with: event)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|