mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
652 lines
27 KiB
Swift
652 lines
27 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import ComponentFlow
|
|
import STCMeshView
|
|
|
|
private final class FPSView: UIView {
|
|
private var lastTimestamp: Double?
|
|
private var counter: Int = 0
|
|
private var fpsValue: Int?
|
|
private var fpsString: NSAttributedString?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.layer.anchorPoint = CGPoint()
|
|
self.backgroundColor = .black
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update() {
|
|
self.counter += 1
|
|
let timestamp = CACurrentMediaTime()
|
|
let deltaTime: Double
|
|
if let lastTimestamp = self.lastTimestamp {
|
|
deltaTime = timestamp - lastTimestamp
|
|
} else {
|
|
deltaTime = 1.0 / 60.0
|
|
self.lastTimestamp = timestamp
|
|
}
|
|
if deltaTime >= 1.0 {
|
|
let fpsValue = Int(Double(self.counter) / deltaTime)
|
|
if self.fpsValue != fpsValue {
|
|
self.fpsValue = fpsValue
|
|
let fpsString = NSAttributedString(string: "\(fpsValue)", attributes: [.foregroundColor: UIColor.white])
|
|
self.bounds = fpsString.boundingRect(with: CGSize(width: 100.0, height: 100.0), context: nil).integral
|
|
self.fpsString = fpsString
|
|
self.setNeedsDisplay()
|
|
}
|
|
self.counter = 0
|
|
self.lastTimestamp = timestamp
|
|
}
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
guard let fpsString = self.fpsString else {
|
|
return
|
|
}
|
|
|
|
fpsString.draw(at: CGPoint())
|
|
}
|
|
}
|
|
|
|
private extension CGPoint {
|
|
static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
|
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
|
|
}
|
|
|
|
static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
|
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
|
|
}
|
|
|
|
static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint {
|
|
return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
|
|
}
|
|
}
|
|
|
|
private func length(_ v: CGPoint) -> CGFloat {
|
|
return sqrt(v.x * v.x + v.y * v.y)
|
|
}
|
|
|
|
private func normalize(_ v: CGPoint) -> CGPoint {
|
|
let len = length(v)
|
|
return CGPoint(x: v.x / len, y: v.y / len)
|
|
}
|
|
|
|
private struct RippleParams {
|
|
var amplitude: CGFloat
|
|
var frequency: CGFloat
|
|
var decay: CGFloat
|
|
var speed: CGFloat
|
|
|
|
init(amplitude: CGFloat, frequency: CGFloat, decay: CGFloat, speed: CGFloat) {
|
|
self.amplitude = amplitude
|
|
self.frequency = frequency
|
|
self.decay = decay
|
|
self.speed = speed
|
|
}
|
|
}
|
|
|
|
private func rippleOffset(
|
|
position: CGPoint,
|
|
origin: CGPoint,
|
|
time: CGFloat,
|
|
params: RippleParams
|
|
) -> CGPoint {
|
|
// The distance of the current pixel position from `origin`.
|
|
let distance: CGFloat = length(position - origin)
|
|
|
|
if distance < 1.0 {
|
|
return position
|
|
}
|
|
|
|
// The amount of time it takes for the ripple to arrive at the current pixel position.
|
|
let delay = distance / params.speed
|
|
|
|
// Adjust for delay, clamp to 0.
|
|
var time = time
|
|
time -= delay
|
|
time = max(0.0, time)
|
|
|
|
// The ripple is a sine wave that Metal scales by an exponential decay
|
|
// function.
|
|
var rippleAmount = params.amplitude * sin(params.frequency * time) * exp(-params.decay * time)
|
|
let absRippleAmount = abs(rippleAmount)
|
|
if rippleAmount < 0.0 {
|
|
rippleAmount = -absRippleAmount
|
|
} else {
|
|
rippleAmount = absRippleAmount
|
|
}
|
|
|
|
if distance <= 60.0 {
|
|
rippleAmount = 0.3 * rippleAmount
|
|
}
|
|
|
|
// A vector of length `amplitude` that points away from position.
|
|
let n: CGPoint
|
|
n = normalize(position - origin)
|
|
|
|
// Scale `n` by the ripple amount at the current pixel position and add it
|
|
// to the current pixel position.
|
|
//
|
|
// This new position moves toward or away from `origin` based on the
|
|
// sign and magnitude of `rippleAmount`.
|
|
return n * (-rippleAmount)
|
|
}
|
|
|
|
func transformToFitQuad2(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) {
|
|
let frameTopLeft = frame.origin
|
|
|
|
let transform = rectToQuad(
|
|
rect: CGRect(origin: CGPoint(), size: frame.size),
|
|
quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y),
|
|
quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y),
|
|
quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y),
|
|
quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y)
|
|
)
|
|
|
|
let anchorPoint = frame.origin
|
|
let anchorOffset = CGPoint(x: anchorPoint.x - frame.origin.x, y: anchorPoint.y - frame.origin.y)
|
|
let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0)
|
|
let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0)
|
|
let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg)
|
|
|
|
return (frame, fullTransform)
|
|
}
|
|
|
|
func transformToFitQuad(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) {
|
|
let boundingBox = boundingBox(forQuadWithTR: tr, tl: tl, bl: bl, br: br)
|
|
|
|
let frameTopLeft = boundingBox.origin
|
|
let transform = rectToQuad(
|
|
rect: CGRect(origin: CGPoint(), size: frame.size),
|
|
quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y),
|
|
quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y),
|
|
quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y),
|
|
quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y)
|
|
)
|
|
|
|
// To account for anchor point, we must translate, transform, translate
|
|
let anchorPoint = frame.center
|
|
let anchorOffset = CGPoint(x: anchorPoint.x - boundingBox.origin.x, y: anchorPoint.y - boundingBox.origin.y)
|
|
let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0)
|
|
let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0)
|
|
let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg)
|
|
|
|
// Now we set our transform
|
|
return (boundingBox, fullTransform)
|
|
}
|
|
|
|
private func boundingBox(forQuadWithTR tr: CGPoint, tl: CGPoint, bl: CGPoint, br: CGPoint) -> CGRect {
|
|
var boundingBox = CGRect.zero
|
|
|
|
let xmin = min(min(min(tr.x, tl.x), bl.x), br.x)
|
|
let ymin = min(min(min(tr.y, tl.y), bl.y), br.y)
|
|
let xmax = max(max(max(tr.x, tl.x), bl.x), br.x)
|
|
let ymax = max(max(max(tr.y, tl.y), bl.y), br.y)
|
|
|
|
boundingBox.origin.x = xmin
|
|
boundingBox.origin.y = ymin
|
|
boundingBox.size.width = xmax - xmin
|
|
boundingBox.size.height = ymax - ymin
|
|
|
|
return boundingBox
|
|
}
|
|
|
|
func rectToQuad(rect: CGRect, quadTL topLeft: CGPoint, quadTR topRight: CGPoint, quadBL bottomLeft: CGPoint, quadBR bottomRight: CGPoint) -> CATransform3D {
|
|
return rectToQuad(rect: rect, quadTLX: topLeft.x, quadTLY: topLeft.y, quadTRX: topRight.x, quadTRY: topRight.y, quadBLX: bottomLeft.x, quadBLY: bottomLeft.y, quadBRX: bottomRight.x, quadBRY: bottomRight.y)
|
|
}
|
|
|
|
private func rectToQuad(rect: CGRect, quadTLX x1a: CGFloat, quadTLY y1a: CGFloat, quadTRX x2a: CGFloat, quadTRY y2a: CGFloat, quadBLX x3a: CGFloat, quadBLY y3a: CGFloat, quadBRX x4a: CGFloat, quadBRY y4a: CGFloat) -> CATransform3D {
|
|
let X = rect.origin.x
|
|
let Y = rect.origin.y
|
|
let W = rect.size.width
|
|
let H = rect.size.height
|
|
|
|
let y21 = y2a - y1a
|
|
let y32 = y3a - y2a
|
|
let y43 = y4a - y3a
|
|
let y14 = y1a - y4a
|
|
let y31 = y3a - y1a
|
|
let y42 = y4a - y2a
|
|
|
|
let a = -H * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42)
|
|
let b = W * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
|
let c = H * X * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) - H * W * x1a * (x4a * y32 - x3a * y42 + x2a * y43) - W * Y * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
|
|
|
let d = H * (-x4a * y21 * y3a + x2a * y1a * y43 - x1a * y2a * y43 - x3a * y1a * y4a + x3a * y2a * y4a)
|
|
let e = W * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42)
|
|
let f = -(W * (x4a * (Y * y2a * y31 + H * y1a * y32) - x3a * (H + Y) * y1a * y42 + H * x2a * y1a * y43 + x2a * Y * (y1a - y3a) * y4a + x1a * Y * y3a * (-y2a + y4a)) - H * X * (x4a * y21 * y3a - x2a * y1a * y43 + x3a * (y1a - y2a) * y4a + x1a * y2a * (-y3a + y4a)))
|
|
|
|
let g = H * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43)
|
|
let h = W * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42)
|
|
var i = W * Y * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42) + H * (X * (-(x3a * y21) + x4a * y21 + x1a * y43 - x2a * y43) + W * (-(x3a * y2a) + x4a * y2a + x2a * y3a - x4a * y3a - x2a * y4a + x3a * y4a))
|
|
|
|
let kEpsilon = 0.0001
|
|
|
|
if abs(i) < kEpsilon {
|
|
i = kEpsilon * (i > 0 ? 1.0 : -1.0)
|
|
}
|
|
|
|
let transform = CATransform3D(
|
|
m11: a / i, m12: d / i, m13: 0, m14: g / i,
|
|
m21: b / i, m22: e / i, m23: 0, m24: h / i,
|
|
m31: 0, m32: 0, m33: 1, m34: 0,
|
|
m41: c / i, m42: f / i, m43: 0, m44: 1.0
|
|
)
|
|
|
|
return transform
|
|
}
|
|
|
|
public protocol SpaceWarpNode: ASDisplayNode {
|
|
var contentNode: ASDisplayNode { get }
|
|
|
|
func triggerRipple(at point: CGPoint)
|
|
func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition)
|
|
}
|
|
|
|
private final class MaskGridLayer: SimpleLayer {
|
|
private var itemLayers: [SimpleLayer] = []
|
|
|
|
private var resolution: (x: Int, y: Int)?
|
|
|
|
func updateGrid(size: CGSize, resolutionX: Int, resolutionY: Int, cornerRadius: CGFloat) {
|
|
if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY {
|
|
return
|
|
}
|
|
self.resolution = (resolutionX, resolutionY)
|
|
|
|
for itemLayer in self.itemLayers {
|
|
itemLayer.removeFromSuperlayer()
|
|
}
|
|
self.itemLayers.removeAll()
|
|
|
|
let itemSize = CGSize(width: size.width / CGFloat(resolutionX), height: size.height / CGFloat(resolutionY))
|
|
|
|
let topLeftCorner = CGRect(origin: CGPoint(), size: CGSize(width: cornerRadius, height: cornerRadius))
|
|
let topRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: 0.0), size: CGSize(width: cornerRadius, height: cornerRadius))
|
|
let bottomLeftCorner = CGRect(origin: CGPoint(x: 0.0, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius))
|
|
let bottomRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius))
|
|
|
|
var cornersImage: UIImage?
|
|
if cornerRadius > 0.0 {
|
|
cornersImage = generateImage(CGSize(width: cornerRadius * 2.0 + 200.0, height: cornerRadius * 2.0 + 200.0), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setFillColor(UIColor.black.cgColor)
|
|
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
|
|
context.fillPath()
|
|
})
|
|
}
|
|
|
|
for y in 0 ..< resolutionY {
|
|
for x in 0 ..< resolutionX {
|
|
let itemLayer = SimpleLayer()
|
|
itemLayer.backgroundColor = UIColor.black.cgColor
|
|
itemLayer.isOpaque = true
|
|
itemLayer.opacity = 1.0
|
|
itemLayer.anchorPoint = CGPoint()
|
|
self.addSublayer(itemLayer)
|
|
self.itemLayers.append(itemLayer)
|
|
|
|
if cornerRadius > 0.0, let cornersImage {
|
|
let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))
|
|
let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize)
|
|
if sourceRect.intersects(topLeftCorner) || sourceRect.intersects(topRightCorner) || sourceRect.intersects(bottomLeftCorner) || sourceRect.intersects(bottomRightCorner) {
|
|
var clippedCornersRect = sourceRect
|
|
if clippedCornersRect.maxX > cornersImage.size.width {
|
|
clippedCornersRect.origin.x -= size.width - cornersImage.size.width
|
|
}
|
|
if clippedCornersRect.maxY > cornersImage.size.height {
|
|
clippedCornersRect.origin.y -= size.height - cornersImage.size.height
|
|
}
|
|
|
|
itemLayer.contents = cornersImage.cgImage
|
|
itemLayer.contentsRect = CGRect(origin: CGPoint(x: clippedCornersRect.minX / cornersImage.size.width, y: clippedCornersRect.minY / cornersImage.size.height), size: CGSize(width: clippedCornersRect.width / cornersImage.size.width, height: clippedCornersRect.height / cornersImage.size.height))
|
|
itemLayer.backgroundColor = nil
|
|
itemLayer.isOpaque = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(positions: [CGPoint], bounds: [CGRect], transforms: [CATransform3D]) {
|
|
for i in 0 ..< self.itemLayers.count {
|
|
if i < positions.count && i < bounds.count && i < transforms.count {
|
|
let itemLayer = self.itemLayers[i]
|
|
itemLayer.position = positions[i]
|
|
itemLayer.bounds = bounds[i]
|
|
itemLayer.transform = transforms[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|
private final class Shockwave {
|
|
let startPoint: CGPoint
|
|
var timeValue: CGFloat = 0.0
|
|
|
|
init(startPoint: CGPoint) {
|
|
self.startPoint = startPoint
|
|
}
|
|
}
|
|
|
|
public var contentNode: ASDisplayNode {
|
|
return self.contentNodeSource
|
|
}
|
|
|
|
private let contentNodeSource: ASDisplayNode
|
|
private let backgroundView: UIView
|
|
private var currentCloneView: UIView?
|
|
private var meshView: STCMeshView?
|
|
|
|
private var gradientLayer: SimpleGradientLayer?
|
|
private var gradientMaskLayer: MaskGridLayer?
|
|
|
|
#if DEBUG
|
|
private var fpsView: FPSView?
|
|
#endif
|
|
|
|
private var link: SharedDisplayLinkDriver.Link?
|
|
|
|
private var shockwaves: [Shockwave] = []
|
|
|
|
private var resolution: (x: Int, y: Int)?
|
|
private var layoutParams: (size: CGSize, cornerRadius: CGFloat)?
|
|
|
|
override public init() {
|
|
self.contentNodeSource = ASDisplayNode()
|
|
|
|
self.backgroundView = UIView()
|
|
self.backgroundView.backgroundColor = .black
|
|
|
|
#if DEBUG && false
|
|
self.fpsView = FPSView(frame: CGRect(origin: CGPoint(x: 4.0, y: 40.0), size: CGSize()))
|
|
#endif
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.contentNodeSource)
|
|
self.view.addSubview(self.backgroundView)
|
|
|
|
#if DEBUG
|
|
if let fpsView = self.fpsView {
|
|
self.view.addSubview(fpsView)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public func triggerRipple(at point: CGPoint) {
|
|
self.shockwaves.append(Shockwave(startPoint: point))
|
|
if self.shockwaves.count > 8 {
|
|
self.shockwaves.removeFirst()
|
|
}
|
|
|
|
if self.link == nil {
|
|
var previousTimestamp = CACurrentMediaTime()
|
|
self.link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let timestamp = CACurrentMediaTime()
|
|
let deltaTime = max(0.0, min(10.0 / 60.0, timestamp - previousTimestamp))
|
|
previousTimestamp = timestamp
|
|
|
|
for shockwave in self.shockwaves {
|
|
shockwave.timeValue += deltaTime * (1.0 / CGFloat(UIView.animationDurationFactor()))
|
|
}
|
|
|
|
if let (size, cornerRadius) = self.layoutParams {
|
|
self.update(size: size, cornerRadius: cornerRadius, transition: .immediate)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private func updateGrid(resolutionX: Int, resolutionY: Int) {
|
|
if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY {
|
|
return
|
|
}
|
|
self.resolution = (resolutionX, resolutionY)
|
|
|
|
if let meshView = self.meshView {
|
|
self.meshView = nil
|
|
meshView.removeFromSuperview()
|
|
}
|
|
|
|
let meshView = STCMeshView(frame: CGRect())
|
|
self.meshView = meshView
|
|
self.view.insertSubview(meshView, aboveSubview: self.backgroundView)
|
|
|
|
meshView.instanceCount = resolutionX * resolutionY
|
|
}
|
|
|
|
public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
|
self.layoutParams = (size, cornerRadius)
|
|
if size.width <= 0.0 || size.height <= 0.0 {
|
|
return
|
|
}
|
|
|
|
self.contentNodeSource.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
|
|
|
let params = RippleParams(amplitude: 10.0, frequency: 15.0, decay: 8.0, speed: 1400.0)
|
|
|
|
if let currentCloneView = self.currentCloneView {
|
|
currentCloneView.removeFromSuperview()
|
|
self.currentCloneView = nil
|
|
}
|
|
|
|
let maxEdge = (max(size.width, size.height) * 0.5) * 2.0
|
|
let maxDistance = sqrt(maxEdge * maxEdge + maxEdge * maxEdge)
|
|
let maxDelay = maxDistance / params.speed
|
|
|
|
for i in (0 ..< self.shockwaves.count).reversed() {
|
|
if self.shockwaves[i].timeValue >= maxDelay {
|
|
self.shockwaves.remove(at: i)
|
|
}
|
|
}
|
|
|
|
guard !self.shockwaves.isEmpty else {
|
|
if let link = self.link {
|
|
self.link = nil
|
|
link.invalidate()
|
|
}
|
|
|
|
if let meshView = self.meshView {
|
|
self.meshView = nil
|
|
meshView.removeFromSuperview()
|
|
}
|
|
|
|
self.resolution = nil
|
|
self.backgroundView.isHidden = true
|
|
self.contentNodeSource.clipsToBounds = false
|
|
self.contentNodeSource.layer.cornerRadius = 0.0
|
|
|
|
if let gradientLayer = self.gradientLayer {
|
|
self.gradientLayer = nil
|
|
gradientLayer.removeFromSuperlayer()
|
|
}
|
|
if let gradientMaskLayer = self.gradientMaskLayer {
|
|
self.gradientMaskLayer = nil
|
|
gradientMaskLayer.removeFromSuperlayer()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
self.backgroundView.isHidden = false
|
|
self.contentNodeSource.clipsToBounds = true
|
|
self.contentNodeSource.layer.cornerRadius = cornerRadius
|
|
|
|
#if DEBUG
|
|
if let fpsView = self.fpsView {
|
|
fpsView.update()
|
|
}
|
|
#endif
|
|
|
|
let resolutionX = max(2, Int(size.width / 40.0))
|
|
let resolutionY = max(2, Int(size.height / 40.0))
|
|
self.updateGrid(resolutionX: resolutionX, resolutionY: resolutionY)
|
|
guard let resolution = self.resolution, let meshView = self.meshView else {
|
|
return
|
|
}
|
|
|
|
if let cloneView = self.contentNodeSource.view.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) {
|
|
self.currentCloneView = cloneView
|
|
meshView.contentView.addSubview(cloneView)
|
|
}
|
|
|
|
meshView.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
if let shockwave = self.shockwaves.first {
|
|
let gradientMaskLayer: MaskGridLayer
|
|
if let current = self.gradientMaskLayer {
|
|
gradientMaskLayer = current
|
|
} else {
|
|
gradientMaskLayer = MaskGridLayer()
|
|
self.gradientMaskLayer = gradientMaskLayer
|
|
}
|
|
|
|
let gradientLayer: SimpleGradientLayer
|
|
if let current = self.gradientLayer {
|
|
gradientLayer = current
|
|
} else {
|
|
gradientLayer = SimpleGradientLayer()
|
|
self.gradientLayer = gradientLayer
|
|
self.layer.addSublayer(gradientLayer)
|
|
|
|
gradientLayer.type = .radial
|
|
gradientLayer.colors = [UIColor(white: 1.0, alpha: 0.0).cgColor, UIColor(white: 1.0, alpha: 0.0).cgColor, UIColor(white: 1.0, alpha: 0.2).cgColor, UIColor(white: 1.0, alpha: 0.0).cgColor]
|
|
|
|
gradientLayer.mask = gradientMaskLayer
|
|
}
|
|
|
|
gradientLayer.frame = CGRect(origin: CGPoint(), size: size)
|
|
gradientMaskLayer.frame = CGRect(origin: CGPoint(), size: size)
|
|
|
|
gradientLayer.startPoint = CGPoint(x: shockwave.startPoint.x / size.width, y: shockwave.startPoint.y / size.height)
|
|
|
|
let distance = shockwave.timeValue * params.speed
|
|
let progress = max(0.0, distance / min(size.width, size.height))
|
|
|
|
let radius = CGSize(width: 1.0 * progress, height: (size.width / size.height) * progress)
|
|
let endEndPoint = CGPoint(x: (gradientLayer.startPoint.x + radius.width), y: (gradientLayer.startPoint.y + radius.height))
|
|
gradientLayer.endPoint = endEndPoint
|
|
|
|
let maxWavefrontNorm: CGFloat = 0.4
|
|
|
|
let normProgress = max(0.0, min(1.0, progress))
|
|
let interpolatedNorm: CGFloat = 1.0 * (1.0 - normProgress) + maxWavefrontNorm * normProgress
|
|
let wavefrontNorm: CGFloat = max(0.01, min(0.99, interpolatedNorm))
|
|
|
|
gradientLayer.locations = ([0.0, 1.0 - wavefrontNorm, 1.0 - wavefrontNorm * 0.5, 1.0] as [CGFloat]).map { $0 as NSNumber }
|
|
|
|
let alphaProgress: CGFloat = max(0.0, min(1.0, normProgress / 0.15))
|
|
var interpolatedAlpha: CGFloat = alphaProgress
|
|
interpolatedAlpha = max(0.0, min(1.0, interpolatedAlpha))
|
|
gradientLayer.opacity = Float(interpolatedAlpha)
|
|
} else {
|
|
if let gradientLayer = self.gradientLayer {
|
|
self.gradientLayer = nil
|
|
gradientLayer.removeFromSuperlayer()
|
|
}
|
|
}
|
|
|
|
let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y))
|
|
|
|
var instanceBounds: [CGRect] = []
|
|
var instancePositions: [CGPoint] = []
|
|
var instanceTransforms: [CATransform3D] = []
|
|
|
|
for y in 0 ..< resolution.y {
|
|
for x in 0 ..< resolution.x {
|
|
let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolution.x), y: CGFloat(y) / CGFloat(resolution.y))
|
|
|
|
let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize)
|
|
|
|
let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY)
|
|
let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY)
|
|
let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY)
|
|
let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY)
|
|
|
|
var topLeft = initialTopLeft
|
|
var topRight = initialTopRight
|
|
var bottomLeft = initialBottomLeft
|
|
var bottomRight = initialBottomRight
|
|
|
|
for shockwave in self.shockwaves {
|
|
topLeft = topLeft + rippleOffset(position: initialTopLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
|
topRight = topRight + rippleOffset(position: initialTopRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
|
bottomLeft = bottomLeft + rippleOffset(position: initialBottomLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
|
bottomRight = bottomRight + rippleOffset(position: initialBottomRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
|
}
|
|
/*topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params)
|
|
topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params)
|
|
bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params)
|
|
bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params)*/
|
|
|
|
let distanceTopLeft = length(topLeft - initialTopLeft)
|
|
let distanceTopRight = length(topRight - initialTopRight)
|
|
let distanceBottomLeft = length(bottomLeft - initialBottomLeft)
|
|
let distanceBottomRight = length(bottomRight - initialBottomRight)
|
|
var maxDistance = max(distanceTopLeft, distanceTopRight)
|
|
maxDistance = max(maxDistance, distanceBottomLeft)
|
|
maxDistance = max(maxDistance, distanceBottomRight)
|
|
|
|
var (frame, transform) = transformToFitQuad2(frame: sourceRect, topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
|
|
|
|
if maxDistance <= 0.005 {
|
|
transform = CATransform3DIdentity
|
|
}
|
|
|
|
instanceBounds.append(frame)
|
|
instancePositions.append(frame.origin)
|
|
|
|
instanceTransforms.append(transform)
|
|
}
|
|
}
|
|
|
|
instanceBounds.withUnsafeMutableBufferPointer { buffer in
|
|
meshView.instanceBounds = buffer.baseAddress!
|
|
}
|
|
instancePositions.withUnsafeMutableBufferPointer { buffer in
|
|
meshView.instancePositions = buffer.baseAddress!
|
|
}
|
|
instanceTransforms.withUnsafeMutableBufferPointer { buffer in
|
|
meshView.instanceTransforms = buffer.baseAddress!
|
|
}
|
|
|
|
if let gradientMaskLayer = self.gradientMaskLayer {
|
|
gradientMaskLayer.updateGrid(size: size, resolutionX: resolutionX, resolutionY: resolutionY, cornerRadius: cornerRadius)
|
|
gradientMaskLayer.update(positions: instancePositions, bounds: instanceBounds, transforms: instanceTransforms)
|
|
}
|
|
}
|
|
|
|
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if self.alpha.isZero || self.isHidden || !self.isUserInteractionEnabled {
|
|
return nil
|
|
}
|
|
for view in self.contentNode.view.subviews.reversed() {
|
|
if let result = view.hitTest(self.view.convert(point, to: view), with: event), result.isUserInteractionEnabled {
|
|
return result
|
|
}
|
|
}
|
|
|
|
let result = super.hitTest(point, with: event)
|
|
if result != self {
|
|
return result
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|