import Foundation import AsyncDisplayKit import Display import Postbox import TelegramPresentationData import GZip private final class ShimmerEffectForegroundNode: ASDisplayNode { private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private let imageNodeContainer: ASDisplayNode private let imageNode: ASImageNode private var absoluteLocation: (CGRect, CGSize)? private var isCurrentlyInHierarchy = false private var shouldBeAnimating = false override init() { self.imageNodeContainer = ASDisplayNode() self.imageNodeContainer.isLayerBacked = true self.imageNode = ASImageNode() self.imageNode.isLayerBacked = true self.imageNode.displaysAsynchronously = false self.imageNode.displayWithoutProcessing = true self.imageNode.contentMode = .scaleToFill super.init() self.isLayerBacked = true self.clipsToBounds = true self.imageNodeContainer.addSubnode(self.imageNode) self.addSubnode(self.imageNodeContainer) } override func didEnterHierarchy() { super.didEnterHierarchy() self.isCurrentlyInHierarchy = true self.updateAnimation() } override func didExitHierarchy() { super.didExitHierarchy() self.isCurrentlyInHierarchy = false self.updateAnimation() } func update(backgroundColor: UIColor, foregroundColor: UIColor) { if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor let image = generateImage(CGSize(width: 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: CGPoint(), size: size)) let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor let peakColor = foregroundColor.cgColor var locations: [CGFloat] = [0.0, 0.5, 1.0] let colors: [CGColor] = [transparentColor, peakColor, transparentColor] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) }) self.imageNode.image = image } func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { return } let sizeUpdated = self.absoluteLocation?.1 != containerSize let frameUpdated = self.absoluteLocation?.0 != rect self.absoluteLocation = (rect, containerSize) if sizeUpdated { if self.shouldBeAnimating { self.imageNode.layer.removeAnimation(forKey: "shimmer") self.addImageAnimation() } else { self.updateAnimation() } } if frameUpdated { self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) } } private func updateAnimation() { let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil if shouldBeAnimating != self.shouldBeAnimating { self.shouldBeAnimating = shouldBeAnimating if shouldBeAnimating { self.addImageAnimation() } else { self.imageNode.layer.removeAnimation(forKey: "shimmer") } } } private func addImageAnimation() { guard let containerSize = self.absoluteLocation?.1 else { return } let gradientHeight: CGFloat = 320.0 self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height)) let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) animation.repeatCount = Float.infinity animation.beginTime = 1.0 self.imageNode.layer.add(animation, forKey: "shimmer") } } class StickerShimmerEffectNode: ASDisplayNode { private let backgroundNode: ASDisplayNode private let effectNode: ShimmerEffectForegroundNode private let foregroundNode: ASImageNode private var maskView: UIImageView? private var currentData: Data? private var currentBackgroundColor: UIColor? private var currentForegroundColor: UIColor? private var currentShimmeringColor: UIColor? private var currentSize = CGSize() override init() { self.backgroundNode = ASDisplayNode() self.effectNode = ShimmerEffectForegroundNode() self.foregroundNode = ASImageNode() super.init() self.addSubnode(self.backgroundNode) self.addSubnode(self.effectNode) self.addSubnode(self.foregroundNode) } public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { self.effectNode.updateAbsoluteRect(rect, within: containerSize) } public func update(backgroundColor: UIColor?, foregroundColor: UIColor, shimmeringColor: UIColor, data: Data?, size: CGSize) { if self.currentData == data, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size { return } self.currentBackgroundColor = backgroundColor self.currentForegroundColor = foregroundColor self.currentShimmeringColor = shimmeringColor self.currentData = data self.currentSize = size self.backgroundNode.backgroundColor = foregroundColor self.effectNode.update(backgroundColor: backgroundColor == nil ? .clear : foregroundColor, foregroundColor: shimmeringColor) let image = generateImage(size, rotatedContext: { size, context in if let backgroundColor = backgroundColor { context.setFillColor(backgroundColor.cgColor) context.setBlendMode(.copy) context.fill(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.clear.cgColor) } else { context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.black.cgColor) } if let data = data, let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024), let path = String(data: unpackedData, encoding: .utf8) { if data.count == 141 { print() } var path = path if !path.hasPrefix("z") { path = "\(path)z" } let reader = PathDataReader(input: path) let segments = reader.read() let scale = size.width / 512.0 context.scaleBy(x: scale, y: scale) renderPath(segments, context: context) } else { let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0)) UIGraphicsPushContext(context) path.fill() UIGraphicsPopContext() } }) if backgroundColor == nil { self.foregroundNode.image = nil let maskView: UIImageView if let current = self.maskView { maskView = current } else { maskView = UIImageView() maskView.frame = CGRect(origin: CGPoint(), size: size) self.maskView = maskView self.view.mask = maskView } } else { self.foregroundNode.image = image if let _ = self.maskView { self.view.mask = nil self.maskView = nil } } self.maskView?.image = image self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size) self.effectNode.frame = CGRect(origin: CGPoint(), size: size) } } open class PathSegment: Equatable { public enum SegmentType { case M case L case C case Q case A case z case H case V case S case T case m case l case c case q case a case h case v case s case t case E case e } public let type: SegmentType public let data: [Double] public init(type: PathSegment.SegmentType = .M, data: [Double] = []) { self.type = type self.data = data } open func isAbsolute() -> Bool { switch type { case .M, .L, .H, .V, .C, .S, .Q, .T, .A, .E: return true default: return false } } public static func == (lhs: PathSegment, rhs: PathSegment) -> Bool { return lhs.type == rhs.type && lhs.data == rhs.data } } private func renderPath(_ segments: [PathSegment], context: CGContext) { var currentPoint: CGPoint? var cubicPoint: CGPoint? var quadrPoint: CGPoint? var initialPoint: CGPoint? func M(_ x: Double, y: Double) { let point = CGPoint(x: CGFloat(x), y: CGFloat(y)) context.move(to: point) setInitPoint(point) } func m(_ x: Double, y: Double) { if let cur = currentPoint { let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) context.move(to: next) setInitPoint(next) } else { M(x, y: y) } } func L(_ x: Double, y: Double) { lineTo(CGPoint(x: CGFloat(x), y: CGFloat(y))) } func l(_ x: Double, y: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)) } else { L(x, y: y) } } func H(_ x: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(x), y: CGFloat(cur.y))) } } func h(_ x: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(cur.y))) } } func V(_ y: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y))) } } func v(_ y: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y) + cur.y)) } } func lineTo(_ p: CGPoint) { context.addLine(to: p) setPoint(p) } func c(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { if let cur = currentPoint { let endPoint = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) let controlPoint1 = CGPoint(x: CGFloat(x1) + cur.x, y: CGFloat(y1) + cur.y) let controlPoint2 = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) setCubicPoint(endPoint, cubic: controlPoint2) } } func C(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { let endPoint = CGPoint(x: CGFloat(x), y: CGFloat(y)) let controlPoint1 = CGPoint(x: CGFloat(x1), y: CGFloat(y1)) let controlPoint2 = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) setCubicPoint(endPoint, cubic: controlPoint2) } func s(_ x2: Double, y2: Double, x: Double, y: Double) { if let cur = currentPoint { let nextCubic = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) let xy1: CGPoint if let curCubicVal = cubicPoint { xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) } else { xy1 = cur } context.addCurve(to: next, control1: xy1, control2: nextCubic) setCubicPoint(next, cubic: nextCubic) } } func S(_ x2: Double, y2: Double, x: Double, y: Double) { if let cur = currentPoint { let nextCubic = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) let next = CGPoint(x: CGFloat(x), y: CGFloat(y)) let xy1: CGPoint if let curCubicVal = cubicPoint { xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) } else { xy1 = cur } context.addCurve(to: next, control1: xy1, control2: nextCubic) setCubicPoint(next, cubic: nextCubic) } } func z() { context.fillPath() } func setQuadrPoint(_ p: CGPoint, quadr: CGPoint) { currentPoint = p quadrPoint = quadr cubicPoint = nil } func setCubicPoint(_ p: CGPoint, cubic: CGPoint) { currentPoint = p cubicPoint = cubic quadrPoint = nil } func setInitPoint(_ p: CGPoint) { setPoint(p) initialPoint = p } func setPoint(_ p: CGPoint) { currentPoint = p cubicPoint = nil quadrPoint = nil } for segment in segments { var data = segment.data switch segment.type { case .M: M(data[0], y: data[1]) data.removeSubrange(Range(uncheckedBounds: (lower: 0, upper: 2))) while data.count >= 2 { L(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .m: m(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) while data.count >= 2 { l(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .L: while data.count >= 2 { L(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .l: while data.count >= 2 { l(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .H: H(data[0]) case .h: h(data[0]) case .V: V(data[0]) case .v: v(data[0]) case .C: while data.count >= 6 { C(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) data.removeSubrange((0 ..< 6)) } case .c: while data.count >= 6 { c(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) data.removeSubrange((0 ..< 6)) } case .S: while data.count >= 4 { S(data[0], y2: data[1], x: data[2], y: data[3]) data.removeSubrange((0 ..< 4)) } case .s: while data.count >= 4 { s(data[0], y2: data[1], x: data[2], y: data[3]) data.removeSubrange((0 ..< 4)) } // case .Q: // Q(data[0], y1: data[1], x: data[2], y: data[3]) // case .q: // q(data[0], y1: data[1], x: data[2], y: data[3]) // case .T: // T(data[0], y: data[1]) // case .t: // t(data[0], y: data[1]) // case .A: // A(data[0], ry: data[1], angle: data[2], largeArc: num2bool(data[3]), sweep: num2bool(data[4]), x: data[5], y: data[6]) // case .a: // a(data[0], ry: data[1], angle: data[2], largeArc: num2bool(data[3]), sweep: num2bool(data[4]), x: data[5], y: data[6]) // case .E: // E(data[0], y: data[1], w: data[2], h: data[3], startAngle: data[4], arcAngle: data[5]) // case .e: // e(data[0], y: data[1], w: data[2], h: data[3], startAngle: data[4], arcAngle: data[5]) case .z: z() default: print("unknown") break } } } private class PathDataReader { private let input: String private var current: UnicodeScalar? private var previous: UnicodeScalar? private var iterator: String.UnicodeScalarView.Iterator private static let spaces: Set = Set("\n\r\t ,".unicodeScalars) init(input: String) { self.input = input self.iterator = input.unicodeScalars.makeIterator() } public func read() -> [PathSegment] { readNext() var segments = [PathSegment]() while let array = readSegments() { segments.append(contentsOf: array) } return segments } private func readSegments() -> [PathSegment]? { if let type = readSegmentType() { let argCount = getArgCount(segment: type) if argCount == 0 { return [PathSegment(type: type)] } var result = [PathSegment]() let data: [Double] if type == .a || type == .A { data = readDataOfASegment() } else { data = readData() } var index = 0 var isFirstSegment = true while index < data.count { let end = index + argCount if end > data.count { break } var currentType = type if type == .M && !isFirstSegment { currentType = .L } if type == .m && !isFirstSegment { currentType = .l } result.append(PathSegment(type: currentType, data: Array(data[index.. [Double] { var data = [Double]() while true { skipSpaces() if let value = readNum() { data.append(value) } else { return data } } } private func readDataOfASegment() -> [Double] { let argCount = getArgCount(segment: .A) var data: [Double] = [] var index = 0 while true { skipSpaces() let value: Double? let indexMod = index % argCount if indexMod == 3 || indexMod == 4 { value = readFlag() } else { value = readNum() } guard let doubleValue = value else { return data } data.append(doubleValue) index += 1 } return data } private func skipSpaces() { var currentCharacter = current while let character = currentCharacter, Self.spaces.contains(character) { currentCharacter = readNext() } } private func readFlag() -> Double? { guard let ch = current else { return .none } readNext() switch ch { case "0": return 0 case "1": return 1 default: return .none } } fileprivate func readNum() -> Double? { guard let ch = current else { return .none } guard ch >= "0" && ch <= "9" || ch == "." || ch == "-" else { return .none } var chars = [ch] var hasDot = ch == "." while let ch = readDigit(&hasDot) { chars.append(ch) } var buf = "" buf.unicodeScalars.append(contentsOf: chars) guard let value = Double(buf) else { return .none } return value } fileprivate func readDigit(_ hasDot: inout Bool) -> UnicodeScalar? { if let ch = readNext() { if (ch >= "0" && ch <= "9") || ch == "e" || (previous == "e" && ch == "-") { return ch } else if ch == "." && !hasDot { hasDot = true return ch } } return nil } fileprivate func isNum(ch: UnicodeScalar, hasDot: inout Bool) -> Bool { switch ch { case "0"..."9": return true case ".": if hasDot { return false } hasDot = true default: return true } return false } @discardableResult private func readNext() -> UnicodeScalar? { previous = current current = iterator.next() return current } private func isAcceptableSeparator(_ ch: UnicodeScalar?) -> Bool { if let ch = ch { return "\n\r\t ,".contains(String(ch)) } return false } private func readSegmentType() -> PathSegment.SegmentType? { while true { if let type = getPathSegmentType() { readNext() return type } if readNext() == nil { return nil } } } fileprivate func getPathSegmentType() -> PathSegment.SegmentType? { if let ch = current { switch ch { case "M": return .M case "m": return .m case "L": return .L case "l": return .l case "C": return .C case "c": return .c case "Q": return .Q case "q": return .q case "A": return .A case "a": return .a case "z", "Z": return .z case "H": return .H case "h": return .h case "V": return .V case "v": return .v case "S": return .S case "s": return .s case "T": return .T case "t": return .t default: break } } return nil } fileprivate func getArgCount(segment: PathSegment.SegmentType) -> Int { switch segment { case .H, .h, .V, .v: return 1 case .M, .m, .L, .l, .T, .t: return 2 case .S, .s, .Q, .q: return 4 case .C, .c: return 6 case .A, .a: return 7 default: return 0 } } }