import Foundation import UIKit import UIKit.UIGestureRecognizerSubclass import SwiftSignalKit import AsyncDisplayKit import Display import AppBundle import LegacyComponents 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 func generateTextMaskImage(size: CGSize, position: CGPoint) -> UIImage? { return generateImage(size, 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 = position context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: min(10.0, min(size.width, size.height) * 0.4), options: .drawsAfterEndLocation) })! } private func generateEmitterMaskImage(size: CGSize, position: CGPoint) -> UIImage? { return generateImage(size, 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 = position context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: min(10.0, min(size.width, size.height) * 0.4), options: .drawsAfterEndLocation) }) } public class InvisibleInkDustNode: ASDisplayNode { private var currentParams: (size: CGSize, color: UIColor, rects: [CGRect], wordRects: [CGRect])? private var animColor: CGColor? 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() self.emitterMaskNode = ASDisplayNode() self.emitterSpotNode = ASImageNode() self.emitterSpotNode.contentMode = .scaleToFill 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.contentsScale = 1.8 emitter.emissionRange = .pi * 2.0 emitter.lifetime = 1.0 emitter.scale = 0.5 emitter.velocityRange = 20.0 emitter.name = "dustCell" emitter.alphaRange = 1.0 // emitter.setValue("point", forKey: "particleType") // emitter.setValue(3.0, forKey: "mass") // emitter.setValue(2.0, forKey: "massRange") self.emitter = emitter let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") fingerAttractor.setValue("fingerAttractor", forKey: "name") let alphaBehavior = createEmitterBehavior(type: "valueOverLife") 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.emitterPosition = CGPoint(x: 0, y: 0) emitterLayer.seed = arc4random() emitterLayer.emitterSize = CGSize(width: 1, height: 1) emitterLayer.emitterShape = CAEmitterLayerEmitterShape(rawValue: "rectangles") emitterLayer.setValue(behaviors, forKey: "emitterBehaviors") // 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 (_, _, _, _) = 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.alpha = 1.0 textNode.view.mask = self.textMaskNode.view self.textSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0) let txtImg = generateTextMaskImage(size: self.emitterNode.frame.size, position: position) self.textSpotNode.image = txtImg let xFactor = (position.x / self.emitterNode.frame.width - 0.5) * 2.0 let yFactor = (position.y / self.emitterNode.frame.height - 0.5) * 2.0 let maxFactor = max(abs(xFactor), abs(yFactor)) var scaleAddition = maxFactor * 4.0 var durationAddition = -maxFactor * 0.2 if self.emitterNode.frame.height > 0.0, self.emitterNode.frame.width / self.emitterNode.frame.height < 0.3 { scaleAddition *= 4.0 durationAddition *= 2.0 } self.textSpotNode.layer.anchorPoint = CGPoint(x: position.x / self.emitterMaskNode.frame.width, y: position.y / self.emitterMaskNode.frame.height) self.textSpotNode.position = position self.textSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { _ in textNode.view.mask = nil }) self.textSpotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) self.emitterNode.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) let img = generateEmitterMaskImage(size: self.emitterNode.frame.size, position: position) self.emitterSpotNode.image = img self.emitterSpotNode.layer.anchorPoint = CGPoint(x: position.x / self.emitterMaskNode.frame.width, y: position.y / self.emitterMaskNode.frame.height) self.emitterSpotNode.position = position self.emitterSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in self?.alpha = 0.0 self?.emitterNode.view.mask = nil self?.emitter?.color = UIColor(rgb: 0x000000).cgColor }) 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 if let (_, color, _, _) = self.currentParams { let colorSpace = CGColorSpaceCreateDeviceRGB() let animation = POPBasicAnimation() animation.property = (POPAnimatableProperty.property(withName: "color", initializer: { property in property?.readBlock = { node, values in if let color = (node as! InvisibleInkDustNode).emitter?.color { if let a = color.components { values?[0] = a[0] values?[1] = a[1] values?[2] = a[2] values?[3] = a[3] } } } property?.writeBlock = { node, values in if let values = values, let color = CGColor(colorSpace: colorSpace, components: values) { let uicolor = UIColor(cgColor: color) print(uicolor) (node as! InvisibleInkDustNode).animColor = color (node as! InvisibleInkDustNode).updateEmitter() } } property?.threshold = 0.4 }) as! POPAnimatableProperty) animation.fromValue = self.emitter?.color animation.toValue = color animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) animation.duration = 0.1 animation.completionBlock = { [weak self] _, _ in if let strongSelf = self { strongSelf.animColor = nil strongSelf.updateEmitter() } } self.pop_add(animation, forKey: "color") } Queue.mainQueue().after(0.15) { 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 = self.animColor ?? 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(120000, square * 0.35) } 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 } } }