Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "InvisibleInkDustNode",
module_name = "InvisibleInkDustNode",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AppBundle:AppBundle",
"//submodules/LegacyComponents:LegacyComponents",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,713 @@
import Foundation
import UIKit
import UIKit.UIGestureRecognizerSubclass
import SwiftSignalKit
import AsyncDisplayKit
import Display
import AppBundle
import LegacyComponents
struct ArbitraryRandomNumberGenerator : RandomNumberGenerator {
init(seed: Int) { srand48(seed) }
func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}
func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? {
var size = originalSize
var position = position
var scale: CGFloat = 1.0
if max(size.width, size.height) > 640.0 {
size = size.aspectFitted(CGSize(width: 640.0, height: 640.0))
scale = size.width / originalSize.width
position = CGPoint(x: position.x * scale, y: position.y * scale)
}
return generateImage(size, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
let startAlpha: CGFloat = inverse ? 0.0 : 1.0
let endAlpha: CGFloat = inverse ? 1.0 : 0.0
var locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0]
let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: startAlpha).cgColor, UIColor(rgb: 0xffffff, alpha: startAlpha).cgColor, UIColor(rgb: 0xffffff, alpha: endAlpha).cgColor, UIColor(rgb: 0xffffff, alpha: endAlpha).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) * scale, options: .drawsAfterEndLocation)
})
}
public class InvisibleInkDustView: UIView {
private var currentParams: (size: CGSize, color: UIColor, textColor: UIColor, rects: [CGRect], wordRects: [CGRect])?
private var animColor: CGColor?
private let enableAnimations: Bool
private weak var textNode: ASDisplayNode?
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
private var staticNode: ASImageNode?
private var staticParams: (size: CGSize, color: UIColor, rects: [CGRect])?
public var isRevealed = false
private var isExploding = false
public init(textNode: ASDisplayNode?, enableAnimations: Bool) {
self.textNode = textNode
self.enableAnimations = enableAnimations
self.emitterNode = ASDisplayNode()
self.emitterNode.isUserInteractionEnabled = false
self.emitterNode.clipsToBounds = true
self.textMaskNode = ASDisplayNode()
self.textMaskNode.isUserInteractionEnabled = false
self.textSpotNode = ASImageNode()
self.textSpotNode.contentMode = .scaleToFill
self.textSpotNode.isUserInteractionEnabled = false
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(frame: .zero)
self.addSubnode(self.emitterNode)
self.textMaskNode.addSubnode(self.textSpotNode)
self.emitterMaskNode.addSubnode(self.emitterSpotNode)
self.emitterMaskNode.addSubnode(self.emitterMaskFillNode)
self.didLoad()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func didLoad() {
if self.enableAnimations {
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 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = CAEmitterCell.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(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.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:))))
}
public func update(revealed: Bool, animated: Bool = true) {
guard self.isRevealed != revealed, let textNode = self.textNode else {
return
}
self.isRevealed = revealed
if revealed {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate
transition.updateAlpha(layer: self.layer, alpha: 0.0)
transition.updateAlpha(node: textNode, alpha: 1.0)
} else {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .linear) : .immediate
transition.updateAlpha(layer: self.layer, alpha: 1.0)
transition.updateAlpha(node: textNode, alpha: 0.0)
if self.isExploding {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
}
}
}
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let (_, _, textColor, _, _) = self.currentParams, let textNode = self.textNode, !self.isRevealed else {
return
}
self.isRevealed = true
if self.enableAnimations {
self.isExploding = true
let position = gestureRecognizer.location(in: self)
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position")
let maskSize = self.emitterNode.frame.size
Queue.concurrentDefaultQueue().async {
let textMaskImage = generateMaskImage(size: maskSize, position: position, inverse: false)
let emitterMaskImage = generateMaskImage(size: maskSize, position: position, inverse: true)
Queue.mainQueue().async {
self.textSpotNode.image = textMaskImage
self.emitterSpotNode.image = emitterMaskImage
}
}
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 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.7 {
scaleAddition *= 5.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)
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 = textColor.cgColor
})
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.textSpotNode.layer.removeAllAnimations()
self.emitterSpotNode.layer.removeAllAnimations()
self.emitterMaskFillNode.layer.removeAllAnimations()
}
} else {
textNode.alpha = 1.0
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.staticNode?.alpha = 0.0
self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
private func updateEmitter() {
guard let (size, color, _, lineRects, wordRects) = self.currentParams else {
return
}
if self.enableAnimations {
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)
}
Queue.mainQueue().async {
self.emitter?.birthRate = min(100000, square * 0.35)
}
} else {
if let staticParams = self.staticParams, staticParams.size == size && staticParams.color == color && staticParams.rects == lineRects && self.staticNode?.image != nil {
return
}
self.staticParams = (size, color, lineRects)
var combinedRect: CGRect?
var combinedRects: [CGRect] = []
for rect in lineRects {
if let currentRect = combinedRect {
if abs(currentRect.minY - rect.minY) < 1.0 && abs(currentRect.maxY - rect.maxY) < 1.0 {
combinedRect = currentRect.union(rect)
} else {
combinedRects.append(currentRect.insetBy(dx: 0.0, dy: -1.0 + UIScreenPixel))
combinedRect = rect
}
} else {
combinedRect = rect
}
}
if let combinedRect {
combinedRects.append(combinedRect.insetBy(dx: 0.0, dy: -1.0))
}
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(color.cgColor)
for rect in combinedRects {
if rect.width > 10.0 {
let rate = Int(rect.width * rect.height * 0.25)
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)
}
}
public func update(size: CGSize, color: UIColor, textColor: UIColor, rects: [CGRect], wordRects: [CGRect]) {
self.currentParams = (size, color, textColor, rects, wordRects)
let bounds = CGRect(origin: CGPoint(), size: size)
self.emitterNode.frame = bounds
self.emitterMaskNode.frame = bounds
self.emitterMaskFillNode.frame = bounds
self.textMaskNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: size)
self.staticNode?.frame = bounds
self.updateEmitter()
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if let (_, _, _, rects, _) = self.currentParams, !self.isRevealed {
for rect in rects {
if rect.contains(point) {
return true
}
}
return false
} else {
return false
}
}
}
public class InvisibleInkDustNode: ASDisplayNode {
private var currentParams: (size: CGSize, color: UIColor, textColor: UIColor, rects: [CGRect], wordRects: [CGRect])?
private var animColor: CGColor?
private let enableAnimations: Bool
public weak var textNode: ASDisplayNode?
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
private var staticNode: ASImageNode?
private var staticParams: (size: CGSize, color: UIColor, rects: [CGRect])?
public var isRevealed = false
private var isExploding = false
public init(textNode: ASDisplayNode?, enableAnimations: Bool) {
self.textNode = textNode
self.enableAnimations = enableAnimations
self.emitterNode = ASDisplayNode()
self.emitterNode.isUserInteractionEnabled = false
self.emitterNode.clipsToBounds = true
self.textMaskNode = ASDisplayNode()
self.textMaskNode.isUserInteractionEnabled = false
self.textSpotNode = ASImageNode()
self.textSpotNode.contentMode = .scaleToFill
self.textSpotNode.isUserInteractionEnabled = false
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.textMaskNode.addSubnode(self.textSpotNode)
self.emitterMaskNode.addSubnode(self.emitterSpotNode)
self.emitterMaskNode.addSubnode(self.emitterMaskFillNode)
}
public override func didLoad() {
super.didLoad()
if self.enableAnimations {
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 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = CAEmitterCell.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(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.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:))))
}
public func update(revealed: Bool, animated: Bool = true) {
guard self.isRevealed != revealed else {
return
}
self.isRevealed = revealed
if revealed {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .linear) : .immediate
transition.updateAlpha(node: self, alpha: 0.0)
if let textNode = self.textNode {
transition.updateAlpha(node: textNode, alpha: 1.0)
}
} else {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .linear) : .immediate
transition.updateAlpha(node: self, alpha: 1.0)
if let textNode = self.textNode {
transition.updateAlpha(node: textNode, alpha: 0.0)
}
if self.isExploding {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
}
}
}
public func revealAtLocation(_ location: CGPoint) {
guard let (_, _, textColor, _, _) = self.currentParams, !self.isRevealed else {
return
}
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 textMaskImage = generateMaskImage(size: maskSize, position: location, inverse: false)
let emitterMaskImage = generateMaskImage(size: maskSize, position: location, inverse: true)
Queue.mainQueue().async {
self.textSpotNode.image = textMaskImage
self.emitterSpotNode.image = emitterMaskImage
}
}
Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { [weak self] in
guard let self else {
return
}
if let textNode = self.textNode {
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 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))
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.7 {
scaleAddition *= 5.0
durationAddition *= 2.0
}
self.textSpotNode.layer.anchorPoint = CGPoint(x: location.x / self.emitterMaskNode.frame.width, y: location.y / self.emitterMaskNode.frame.height)
self.textSpotNode.position = location
self.textSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.55 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in
guard let self else {
return
}
if let textNode = self.textNode {
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)
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.55 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in
self?.alpha = 0.0
self?.emitterNode.view.mask = nil
self?.emitter?.color = textColor.cgColor
})
self.emitterMaskFillNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.textSpotNode.layer.removeAllAnimations()
self.emitterSpotNode.layer.removeAllAnimations()
self.emitterMaskFillNode.layer.removeAllAnimations()
}
} else {
if let textNode = self.textNode {
textNode.alpha = 1.0
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
self.staticNode?.alpha = 0.0
self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
public func revealWithoutMaskAtLocation(_ location: CGPoint) {
guard !self.isRevealed else {
return
}
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")
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
}
} else {
self.staticNode?.alpha = 0.0
self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self.view)
self.revealAtLocation(location)
}
private func updateEmitter() {
guard let (size, color, _, lineRects, wordRects) = self.currentParams else {
return
}
if self.enableAnimations {
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)
}
Queue.mainQueue().async {
self.emitter?.birthRate = min(100000, square * 0.35)
}
} else {
if let staticParams = self.staticParams, staticParams.size == size && staticParams.color == color && staticParams.rects == lineRects && self.staticNode?.image != nil {
return
}
self.staticParams = (size, color, lineRects)
var combinedRect: CGRect?
var combinedRects: [CGRect] = []
for rect in lineRects {
if let currentRect = combinedRect {
if abs(currentRect.minY - rect.minY) < 1.0 && abs(currentRect.maxY - rect.maxY) < 1.0 {
combinedRect = currentRect.union(rect)
} else {
combinedRects.append(currentRect.insetBy(dx: 0.0, dy: -1.0 + UIScreenPixel))
combinedRect = rect
}
} else {
combinedRect = rect
}
}
if let combinedRect {
combinedRects.append(combinedRect.insetBy(dx: 0.0, dy: -1.0))
}
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(color.cgColor)
for rect in combinedRects {
if rect.width > 10.0 {
let rate = Int(rect.width * rect.height * 0.25)
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)
}
}
public func update(size: CGSize, color: UIColor, textColor: UIColor, rects: [CGRect], wordRects: [CGRect]) {
self.currentParams = (size, color, textColor, rects, wordRects)
let bounds = CGRect(origin: CGPoint(), size: size)
self.emitterNode.frame = bounds
self.emitterMaskNode.frame = bounds
self.emitterMaskFillNode.frame = bounds
self.textMaskNode.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: size)
self.staticNode?.frame = bounds
if self.isNodeLoaded {
self.updateEmitter()
}
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if let (_, _, _, rects, _) = self.currentParams, !self.isRevealed {
for rect in rects {
if rect.contains(point) {
return true
}
}
return false
} else {
return false
}
}
}
@@ -0,0 +1,429 @@
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 revealOnTap = true
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()
guard self.revealOnTap else {
return
}
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
}
}
}