Swiftgram/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift
2021-12-23 13:18:00 +04:00

268 lines
12 KiB
Swift

import Foundation
import UIKit
import UIKit.UIGestureRecognizerSubclass
import SwiftSignalKit
import AsyncDisplayKit
import Display
import AppBundle
private func createEmitterBehavior(type: String) -> NSObject {
let selector = ["behaviorWith", "Type:"].joined(separator: "")
let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type
let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))!
let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self)
return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type)
}
private let textMaskImage: UIImage = {
return generateImage(CGSize(width: 60.0, height: 60.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
var locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width / 2.0, options: .drawsAfterEndLocation)
})!
}()
private let emitterMaskImage: UIImage = {
return generateImage(CGSize(width: 120.0, height: 120.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
var locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width / 10.0, options: .drawsAfterEndLocation)
})!
}()
public class InvisibleInkDustNode: ASDisplayNode {
private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect])?
private weak var textNode: TextNode?
private let textMaskNode: ASDisplayNode
private let textSpotNode: ASImageNode
private var emitterNode: ASDisplayNode
private var emitter: CAEmitterCell?
private var emitterLayer: CAEmitterLayer?
private let emitterMaskNode: ASDisplayNode
private let emitterSpotNode: ASImageNode
private let emitterMaskFillNode: ASDisplayNode
public var isRevealed = false
public init(textNode: TextNode?) {
self.textNode = textNode
self.emitterNode = ASDisplayNode()
self.emitterNode.clipsToBounds = true
self.textMaskNode = ASDisplayNode()
self.textSpotNode = ASImageNode()
let img = textMaskImage
self.textSpotNode.image = img
self.emitterMaskNode = ASDisplayNode()
self.emitterSpotNode = ASImageNode()
let simg = emitterMaskImage
self.emitterSpotNode.image = simg
self.emitterMaskFillNode = ASDisplayNode()
self.emitterMaskFillNode.backgroundColor = .white
super.init()
self.addSubnode(self.emitterNode)
self.textMaskNode.addSubnode(self.textSpotNode)
self.emitterMaskNode.addSubnode(self.emitterSpotNode)
self.emitterMaskNode.addSubnode(self.emitterMaskFillNode)
}
public override func didLoad() {
super.didLoad()
let emitter = CAEmitterCell()
emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage
emitter.setValue(1.8, forKey: "contentsScale")
emitter.emissionRange = .pi * 2.0
emitter.setValue(3.0, forKey: "mass")
emitter.setValue(2.0, forKey: "massRange")
emitter.lifetime = 1.0
emitter.scale = 0.5
emitter.velocityRange = 20.0
emitter.name = "dustCell"
emitter.setValue("point", forKey: "particleType")
emitter.color = UIColor.white.withAlphaComponent(0.0).cgColor
emitter.alphaRange = 1.0
self.emitter = emitter
let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("alphaBehavior", forKey: "name")
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive")
let behaviors = [fingerAttractor, alphaBehavior]
let emitterLayer = CAEmitterLayer()
emitterLayer.masksToBounds = true
emitterLayer.allowsGroupOpacity = true
emitterLayer.lifetime = 1
emitterLayer.emitterCells = [emitter]
emitterLayer.setValue(behaviors, forKey: "emitterBehaviors")
emitterLayer.emitterPosition = CGPoint(x: 0, y: 0)
emitterLayer.seed = arc4random()
emitterLayer.setValue("rectangles", forKey: "emitterShape")
emitterLayer.emitterSize = CGSize(width: 1, height: 1)
emitterLayer.setValue(0.0322, forKey: "updateInterval")
emitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.fingerAttractor.stiffness")
emitterLayer.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.emitterLayer = emitterLayer
self.emitterNode.layer.addSublayer(emitterLayer)
self.updateEmitter()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:))))
}
public func update(revealed: Bool) {
guard self.isRevealed != revealed, let textNode = self.textNode else {
return
}
self.isRevealed = revealed
if revealed {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear)
transition.updateAlpha(node: self, alpha: 0.0)
transition.updateAlpha(node: textNode, alpha: 1.0)
} else {
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear)
transition.updateAlpha(node: self, alpha: 1.0)
transition.updateAlpha(node: textNode, alpha: 0.0)
}
}
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let (size, _, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else {
return
}
self.isRevealed = true
let position = gestureRecognizer.location(in: self.view)
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position")
Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) {
textNode.view.mask = self.textMaskNode.view
textNode.alpha = 1.0
let radius = max(size.width, size.height)
self.textSpotNode.frame = CGRect(x: position.x - radius / 2.0, y: position.y - radius / 2.0, width: radius, height: radius)
self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.textSpotNode.layer.animateScale(from: 0.1, to: 3.5, duration: 0.71, removeOnCompletion: false, completion: { _ in
textNode.view.mask = nil
})
self.emitterNode.view.mask = self.emitterMaskNode.view
let emitterSide = radius * 5.0
self.emitterSpotNode.frame = CGRect(x: position.x - emitterSide / 2.0, y: position.y - emitterSide / 2.0, width: emitterSide, height: emitterSide)
self.emitterSpotNode.layer.animateScale(from: 0.1, to: 3.0, duration: 0.71, removeOnCompletion: false, completion: { [weak self] _ in
self?.alpha = 0.0
self?.emitterNode.view.mask = nil
})
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.textSpotNode.layer.removeAllAnimations()
self.emitterSpotNode.layer.removeAllAnimations()
self.emitterMaskFillNode.layer.removeAllAnimations()
}
var spoilersLength: Int = 0
if let spoilers = textNode.cachedLayout?.spoilers {
for spoiler in spoilers {
spoilersLength += spoiler.0.length
}
}
let timeToRead = min(45.0, ceil(max(4.0, Double(spoilersLength) * 0.04)))
Queue.mainQueue().after(timeToRead * UIView.animationDurationFactor()) {
self.isRevealed = false
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .linear)
transition.updateAlpha(node: self, alpha: 1.0)
transition.updateAlpha(node: textNode, alpha: 0.0)
}
}
private func updateEmitter() {
guard let (size, color, _, wordRects) = self.currentParams else {
return
}
self.emitter?.color = color.cgColor
self.emitterLayer?.setValue(wordRects, forKey: "emitterRects")
self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size)
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")
var square: Float = 0.0
for rect in wordRects {
square += Float(rect.width * rect.height)
}
self.emitter?.birthRate = min(100000, square * 0.33)
}
public func update(size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect]) {
self.currentParams = (size, color, rects, wordRects)
self.emitterNode.frame = CGRect(origin: CGPoint(), size: size)
self.emitterMaskNode.frame = self.emitterNode.bounds
self.emitterMaskFillNode.frame = self.emitterNode.bounds
self.textMaskNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: size)
if self.isNodeLoaded {
self.updateEmitter()
}
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if let (_, _, rects, _) = self.currentParams {
for rect in rects {
if rect.contains(point) {
return true
}
}
return false
} else {
return false
}
}
}