// // PrivadoLoader.swift // PrivadoVPN // // Created by Murad Shabanov on 08.09.2021. // Copyright © 2021 Privado LLC. All rights reserved. // import Foundation import AppKit final class PrivadoLoader: NSControl { public var arcColor = NSColor(red: 80, green: 90, blue: 190) { didSet { self.updateLayer() } } public var keyholeColor = NSColor(red: 80, green: 90, blue: 190) { didSet { self.updateLayer() } } public var arcWidth: CGFloat = 5 { didSet { self.updateLayer() } } public var backCircleColor: NSColor = .clear { didSet { self.updateLayer() } } public var hasShadow: Bool = false { didSet { self.updateLayer() } } public var connecting: Bool = false public var isDisconnected = true { didSet { if isDisconnected { self.startTaphereTimer() } } } // MARK: Initialization override init(frame frameRect: NSRect) { super.init(frame: frameRect) } init(frame frameRect: NSRect, isButton: Bool) { super.init(frame: frameRect) self.wantsLayer = true self.isEnabled = false self.layer?.masksToBounds = false self.layer?.removeAllAnimations() self.layer?.sublayers?.forEach { $0.removeAllAnimations() } self.bindUI() self.setup() self.arcLayers = [upperLargeArc, rightLargeArc, leftLargeArc, rightMiddleArc, leftMiddleArc, leftLowArc, upperLowArc, bottomLowArc] self.largeArcLayers = [upperLargeArc, rightLargeArc, leftLargeArc] self.middleArcLayers = [leftMiddleArc, rightMiddleArc] self.lowArcLayers = [leftLowArc, upperLowArc, bottomLowArc] if isButton { self.startTaphereTimer() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private var backgroundCircle = CAShapeLayer() private var taphereLayer = CAShapeLayer() private var upperLargeArc = ConnectionLayer() private var rightLargeArc = ConnectionLayer() private var leftLargeArc = ConnectionLayer() private var rightMiddleArc = ConnectionLayer() private var leftMiddleArc = ConnectionLayer() private var leftLowArc = ConnectionLayer() private var upperLowArc = ConnectionLayer() private var bottomLowArc = ConnectionLayer() private var keyholeLayer = ConnectionLayer() private var arcLayers = [ConnectionLayer]() private var largeArcLayers = [ConnectionLayer]() private var middleArcLayers = [ConnectionLayer]() private var lowArcLayers = [ConnectionLayer]() private var trackingArea: NSTrackingArea? private var wasAnimated = false private var clockwise = false private var currentColor: NSColor? private var totalDuration = 0.0 private let duration = 1.0 private let secondDuration = 0.5 weak var taphereTimer: Timer? private let loaderBack = NSColor(red: 46, green: 51, blue: 80) private let tapHereBackColor = NSColor(red: 71, green: 80, blue: 130) private let whiteColor = NSColor.white let yellowColor = NSColor(red: 255, green: 216, blue: 8) let redColor = NSColor(red: 229, green: 93, blue: 129) let orangeColor = NSColor(red: 254, green: 147, blue: 67) let ligtGreenColor = NSColor(red: 148, green: 215, blue: 80) let mediumGreenColor = NSColor(red: 94, green: 215, blue: 117) let greenColor = NSColor(red: 40, green: 215, blue: 153) public func startAnimation(clockwise: Bool) { if !self.wasAnimated { self.animation(clockwise: clockwise) } } public func colorChangeAnimaton(duration: Double = 2, color: NSColor) { if self.currentColor != color { self.keyholeColorChangeAnimation(duration: duration, color: color) self.currentColor = color } } public func animation(clockwise: Bool) { self.firstRotation(for: self.upperLargeArc, clockwise: clockwise) self.firstRotation(for: self.leftLargeArc, clockwise: clockwise) self.firstRotation(for: self.rightLargeArc, clockwise: clockwise) self.firstRotation(for: self.leftMiddleArc, clockwise: !clockwise) self.firstRotation(for: self.rightMiddleArc, clockwise: !clockwise) self.firstRotation(for: self.leftLowArc, clockwise: clockwise) self.firstRotation(for: self.bottomLowArc, clockwise: clockwise) self.firstRotation(for: self.upperLowArc, clockwise: clockwise) self.wasAnimated = true self.clockwise = clockwise } // maybe will be needed public func stopAnimation() { // self.pauseLayer(self.lowRingLayer) // self.pauseLayer(self.middleRingLayer) // self.pauseLayer(self.largeRingLayer) } override func updateLayer() { super.updateLayer() self.updateViewAppearance() } override func layout() { super.layout() self.setup() } private func bindUI() { self.layer?.addSublayer(self.backgroundCircle) self.backgroundCircle.addSublayer(self.rightLargeArc) self.backgroundCircle.addSublayer(self.leftLargeArc) self.backgroundCircle.addSublayer(self.upperLargeArc) self.backgroundCircle.addSublayer(self.leftMiddleArc) self.backgroundCircle.addSublayer(self.rightMiddleArc) self.backgroundCircle.addSublayer(self.upperLowArc) self.backgroundCircle.addSublayer(self.leftLowArc) self.backgroundCircle.addSublayer(self.bottomLowArc) self.backgroundCircle.addSublayer(self.keyholeLayer) } private func setup() { self.setupArc(self.upperLargeArc, radius: self.bounds.height * 0.48, startAngle: 150, endAngle: 35) self.setupArc(self.leftLargeArc, radius: self.bounds.height * 0.48, startAngle: 225, endAngle: 165) self.setupArc(self.rightLargeArc, radius: self.bounds.height * 0.48, startAngle: 15, endAngle: 240) self.setupArc(self.leftMiddleArc, radius: self.bounds.height * 0.4, startAngle: 270, endAngle: 75) self.setupArc(self.rightMiddleArc, radius: self.bounds.height * 0.4, startAngle: 60, endAngle: 285) self.setupArc(self.upperLowArc, radius: self.bounds.height * 0.3, startAngle: 150, endAngle: 330) self.setupArc(self.leftLowArc, radius: self.bounds.height * 0.3, startAngle: 225, endAngle: 180) self.setupArc(self.bottomLowArc, radius: self.bounds.height * 0.3, startAngle: 315, endAngle: 240) self.setupBackCircle() self.setupKeyholeLayer() } private func setupKeyholeLayer() { let path = NSBezierPath() path.appendArc(withCenter: NSPoint(x: self.bounds.width * 0.5, y: self.bounds.height * 0.54), radius: self.bounds.height * 0.08, startAngle: 240, endAngle: 300, clockwise: true) path.line(to: NSPoint(x: self.bounds.width * 0.54, y: self.bounds.height * 0.42)) path.appendArc( withCenter: NSPoint(x: self.bounds.width * 0.50, y: self.bounds.height * 0.42), radius: self.bounds.height * 0.04, startAngle: 0, endAngle: 180, clockwise: true) let circle = NSBezierPath() circle.appendArc(withCenter: NSPoint(x: self.bounds.midX, y: self.bounds.midY), radius: self.bounds.height * 0.23, startAngle: 0, endAngle: 360) let allPathes = NSBezierPath() allPathes.append(path) allPathes.append(circle) self.keyholeLayer.frame = self.bounds self.keyholeLayer.path = allPathes.CGPath self.keyholeLayer.fillColor = self.keyholeColor.cgColor self.keyholeLayer.fillRule = .evenOdd } private func updateViewAppearance() { self.backgroundCircle.fillColor = self.backCircleColor.cgColor self.keyholeLayer.fillColor = self.keyholeColor.cgColor } private func setupBackCircle() { let circle = NSBezierPath() circle.appendArc(withCenter: self.bounds.center, radius: self.bounds.height * 0.6, startAngle: 0, endAngle: 360) self.backgroundCircle.shadowRadius = 9 self.backgroundCircle.shadowColor = self.hasShadow ? .black : .clear self.backgroundCircle.shadowOpacity = 0.6 self.backgroundCircle.shadowOffset = CGSize(width: 0, height: 0) self.backgroundCircle.lineWidth = 2 self.backgroundCircle.strokeColor = .clear self.backgroundCircle.path = circle.CGPath self.backgroundCircle.frame = self.bounds } private func setupArc(_ layer: ConnectionLayer, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) { let arc = NSBezierPath() arc.appendArc(withCenter: NSPoint(x: self.bounds.midX, y: self.bounds.midY), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) layer.path = arc.CGPath layer.fillColor = nil layer.frame = self.bounds layer.lineCap = .round layer.newColor = self.arcColor layer.arcWidth = self.arcWidth } // MARK: TaphereTimer func startTaphereTimer() { // self.taphereTimer?.invalidate() // self.taphereTimer = Timer.scheduledTimer(timeInterval: 35.0, target: self, selector: #selector(self.timerHandler), userInfo: nil, repeats: false) } func stopTaphereTimer() { // self.taphereTimer?.invalidate() } @objc func timerHandler() { if self.isDisconnected { self.tapHereAnimation(duration: 1.0, repeatCount: 2.0) self.startTaphereTimer() } else { self.stopTaphereTimer() } } // MARK: Animations private func keyholeColorChangeAnimation(duration: Double, color: NSColor) { let fillColorAnimation = CABasicAnimation(keyPath: "fillColor") fillColorAnimation.duration = duration fillColorAnimation.fromValue = self.keyholeLayer.fillColor fillColorAnimation.toValue = color.cgColor self.keyholeColor = color self.keyholeLayer.add(fillColorAnimation, forKey: "fillColor") } private func arcChangeColorAnimation(layer: ConnectionLayer, duration: Double, delay: Double, from color: NSColor, to newColor: NSColor, repeatCount: Float) { let strokeColorAnimation = CABasicAnimation(keyPath: "strokeColor") strokeColorAnimation.duration = duration strokeColorAnimation.fromValue = color.cgColor strokeColorAnimation.toValue = newColor.cgColor strokeColorAnimation.beginTime = 0 layer.newColor = newColor let animation = CABasicAnimation(keyPath: "strokeColor") animation.duration = duration animation.fromValue = newColor.cgColor animation.toValue = color.cgColor animation.beginTime = duration layer.newColor = color let fakeAnimation = CABasicAnimation(keyPath: "strokeColor") fakeAnimation.duration = delay - duration * 2 fakeAnimation.fromValue = color.cgColor fakeAnimation.toValue = color.cgColor fakeAnimation.beginTime = duration * 2 layer.newColor = color let animationGroup = CAAnimationGroup() animationGroup.animations = [strokeColorAnimation, animation, fakeAnimation] animationGroup.repeatCount = repeatCount animationGroup.duration = delay + duration * 2 layer.add(animationGroup, forKey: "connectingAnimation") } private func firstRotation(for layer: CALayer, clockwise: Bool) { let angle = clockwise ? -CGFloat.pi * 2: CGFloat.pi * 2 let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotateAnimation.fromValue = 0 rotateAnimation.toValue = angle rotateAnimation.duration = 9 rotateAnimation.repeatCount = .infinity layer.add(rotateAnimation, forKey: "layer") return } private func changeRotationSpeed(speed: Float) { self.arcLayers.forEach { layer in layer.timeOffset = layer.convertTime(CACurrentMediaTime(), from: nil) layer.beginTime = CACurrentMediaTime() layer.speed = speed } } private func removeConnectingAnimation() { self.largeArcLayers.forEach { $0.removeAnimation(forKey: "connectingAnimation") } self.middleArcLayers.forEach { $0.removeAnimation(forKey: "connectingAnimation") } self.lowArcLayers.forEach { $0.removeAnimation(forKey: "connectingAnimation") } } public func startConnectionAnimation(completion: @escaping () -> Void) { self.isDisconnected = false self.backgroundCircle.removeAnimation(forKey: "taphereanimation") self.changeRotationSpeed(speed: 1.8) // pulse red color self.arcChangeColorAnimation(layer: self.rightLargeArc, duration: self.duration, delay: 0.0, from: self.arcColor, to: self.redColor, repeatCount: 1) self.arcChangeColorAnimation(layer: self.leftMiddleArc, duration: self.duration, delay: 0.0, from: self.arcColor, to: self.redColor, repeatCount: 1) self.arcChangeColorAnimation(layer: self.bottomLowArc, duration: self.duration, delay: 0.0, from: self.arcColor, to: self.redColor, repeatCount: 1) self.totalDuration += duration * 2 // pulse orange color DispatchQueue.main.asyncAfter(deadline: .now() + self.totalDuration) { guard self.connecting else { return } self.keyholeColorChangeAnimation(duration: self.duration, color: self.orangeColor) self.arcChangeColorAnimation(layer: self.rightLargeArc, duration: self.duration, delay: self.totalDuration, from: self.arcColor, to: self.orangeColor, repeatCount: 1) self.arcChangeColorAnimation(layer: self.leftMiddleArc, duration: self.duration, delay: self.totalDuration, from: self.arcColor, to: self.orangeColor, repeatCount: 1) self.arcChangeColorAnimation(layer: self.bottomLowArc, duration: self.duration, delay: self.totalDuration, from: self.arcColor, to: self.orangeColor, repeatCount: 1) } self.totalDuration += duration * 2 // pulse yellow color DispatchQueue.main.asyncAfter(deadline: .now() + self.totalDuration) { guard self.connecting else { return } self.keyholeColorChangeAnimation(duration: self.duration, color: self.yellowColor) self.arcChangeColorAnimation(layer: self.rightLargeArc, duration: self.duration, delay: self.totalDuration, from: self.arcColor, to: self.yellowColor, repeatCount: 1) self.arcChangeColorAnimation(layer: self.leftMiddleArc, duration: self.duration, delay: self.totalDuration, from: self.arcColor, to: self.yellowColor, repeatCount: 1) self.arcChangeColorAnimation(layer: self.bottomLowArc, duration: self.duration, delay: self.totalDuration, from: self.arcColor, to: self.yellowColor, repeatCount: 1) DispatchQueue.main.asyncAfter(deadline: .now() + self.duration * 2) { completion() } } self.totalDuration = 0.0 } public func tapHereAnimation(duration: Double, repeatCount: Float) { self.configureTapHereLayer() self.backgroundCircle.lineWidth = 2 self.backgroundCircle.name = "backCircle" self.keyholeLayer.name = "keyholeLayer" self.taphereLayer.name = "taphereLayer" // change background circle animation let backFillColorAnimation = CABasicAnimation(keyPath: "fillColor") backFillColorAnimation.duration = duration backFillColorAnimation.fromValue = self.loaderBack.cgColor backFillColorAnimation.toValue = self.tapHereBackColor.cgColor backFillColorAnimation.beginTime = 0 // change keyhole layer animation let fillColorAnimation = CABasicAnimation(keyPath: "sublayers.keyholeLayer.fillColor") fillColorAnimation.duration = duration fillColorAnimation.fromValue = self.keyholeColor.cgColor fillColorAnimation.toValue = self.whiteColor.cgColor fillColorAnimation.beginTime = 0 // pulse stroke line background circle let backCircleStrokePulse = CABasicAnimation(keyPath: "strokeColor") backCircleStrokePulse.duration = duration backCircleStrokePulse.fromValue = NSColor.clear.cgColor backCircleStrokePulse.toValue = NSColor.white.cgColor backCircleStrokePulse.beginTime = 0 // tap here layer appear animatino let tapHereShowAniamtion = CABasicAnimation(keyPath: "sublayers.taphereLayer.opacity") tapHereShowAniamtion.duration = duration tapHereShowAniamtion.fromValue = 0 tapHereShowAniamtion.toValue = 1 tapHereShowAniamtion.beginTime = 0 // and evetything back let oneAnimation = CABasicAnimation(keyPath: "fillColor") oneAnimation.duration = duration oneAnimation.fromValue = self.tapHereBackColor.cgColor oneAnimation.toValue = self.loaderBack.cgColor oneAnimation.beginTime = duration let secondAnimation = CABasicAnimation(keyPath: "sublayers.keyholeLayer.fillColor") secondAnimation.duration = duration secondAnimation.fromValue = self.whiteColor.cgColor secondAnimation.toValue = self.keyholeColor.cgColor secondAnimation.beginTime = duration let thirdAnimation = CABasicAnimation(keyPath: "strokeColor") thirdAnimation.duration = duration thirdAnimation.fromValue = NSColor.white.cgColor thirdAnimation.toValue = NSColor.clear.cgColor thirdAnimation.beginTime = duration let forthAnimation = CABasicAnimation(keyPath: "sublayers.taphereLayer.opacity") forthAnimation.duration = duration forthAnimation.fromValue = 1 forthAnimation.toValue = 0 forthAnimation.beginTime = duration let animationGroup = CAAnimationGroup() animationGroup.animations = [fillColorAnimation, secondAnimation, backCircleStrokePulse, thirdAnimation, tapHereShowAniamtion, forthAnimation, backFillColorAnimation, oneAnimation] animationGroup.duration = duration * 2 animationGroup.repeatCount = repeatCount self.backgroundCircle.add(animationGroup, forKey: "taphereanimation") } private func configureTapHereLayer() { let backRect = CGRect(x: self.bounds.width / 4, y: self.bounds.height / 7, width: self.bounds.width / 2, height: self.bounds.height / 6) let backLayer = CAShapeLayer() let path = NSBezierPath(roundedRect: backRect, xRadius: 7, yRadius: 7) backLayer.path = path.CGPath backLayer.fillColor = .white backLayer.opacity = 0 let textLayer = CATextLayer() let width = backRect.width let heigght = backRect.height * 0.8 let x = backRect.origin.x * 1.3 let y = backRect.origin.y textLayer.frame = CGRect(x: x, y: y, width: width, height: heigght) textLayer.string = "Tap here" textLayer.fontSize = 7 if let scale = NSScreen.main?.backingScaleFactor { textLayer.contentsScale = scale } textLayer.foregroundColor = .black backLayer.addSublayer(textLayer) self.taphereLayer = backLayer self.backgroundCircle.addSublayer(self.taphereLayer) } public func connectingAnimation() { self.removeConnectingAnimation() var delay = 0.0 self.largeArcLayers.forEach { layer in DispatchQueue.main.asyncAfter(deadline: .now() + delay) { guard self.connecting else { return } self.arcChangeColorAnimation(layer: layer, duration: self.duration, delay: self.duration * 4, from: self.arcColor, to: self.yellowColor, repeatCount: .infinity) } delay += self.duration * 2 } var middleDelay = 0.0 self.middleArcLayers.forEach { layer in DispatchQueue.main.asyncAfter(deadline: .now() + middleDelay) { guard self.connecting else { return } self.arcChangeColorAnimation(layer: layer, duration: self.duration, delay: self.duration * 2, from: self.arcColor, to: self.yellowColor, repeatCount: .infinity) } middleDelay += self.duration * 2 } var lowDelay = 0.0 self.lowArcLayers.forEach { layer in DispatchQueue.main.asyncAfter(deadline: .now() + lowDelay) { guard self.connecting else { return } self.arcChangeColorAnimation(layer: layer, duration: self.duration, delay: self.duration * 4, from: self.arcColor, to: self.yellowColor, repeatCount: .infinity) } lowDelay += self.duration * 2 } } public func connectedAnimation() { self.removeConnectingAnimation() self.changeRotationSpeed(speed: 1.0) self.totalDuration = 0.0 // pulse green small self.keyholeColorChangeAnimation(duration: self.duration, color: self.ligtGreenColor) self.lowArcLayers.forEach { self.arcChangeColorAnimation(layer: $0, duration: self.duration, delay: 0, from: self.arcColor, to: self.ligtGreenColor, repeatCount: 1) } self.totalDuration += secondDuration // pulse green medium DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { guard !self.isDisconnected else { return } self.keyholeColorChangeAnimation(duration: self.duration, color: self.mediumGreenColor) self.middleArcLayers.forEach { self.arcChangeColorAnimation(layer: $0, duration: self.duration, delay: 0, from: self.arcColor, to: self.mediumGreenColor, repeatCount: 1) } } self.totalDuration += secondDuration // pulse green large DispatchQueue.main.asyncAfter(deadline: .now() + totalDuration) { guard !self.isDisconnected else { return } self.keyholeColorChangeAnimation(duration: self.duration, color: self.greenColor) self.largeArcLayers.forEach { self.arcChangeColorAnimation(layer: $0, duration: self.duration, delay: 0, from: self.arcColor, to: self.greenColor, repeatCount: 1) } } self.totalDuration = 0.0 } public func disconnectAnimation() { self.isDisconnected = true self.changeRotationSpeed(speed: 1.0) self.removeConnectingAnimation() self.largeArcLayers.forEach {self.arcChangeColorAnimation(layer: $0, duration: 0.5, delay: 0.0, from: $0.newColor, to: self.redColor, repeatCount: 0)} self.middleArcLayers.forEach {self.arcChangeColorAnimation(layer: $0, duration: 0.5, delay: 0.0, from: $0.newColor, to: self.redColor, repeatCount: 0)} self.lowArcLayers.forEach {self.arcChangeColorAnimation(layer: $0, duration: 0.5, delay: 0.0, from: $0.newColor, to: self.redColor, repeatCount: 0)} self.keyholeColorChangeAnimation(duration: 0.5, color: self.redColor) } // maybe will be needed private func continueRotation(for layer: CAShapeLayer, clockwise: Bool) { guard let presentation = layer.presentation() else { return } let angle = clockwise ? -CGFloat.pi * 2: CGFloat.pi * 2 let transform = presentation.transform let current = atan2(transform.m12, transform.m11) // layer.transform = CATransform3DRotate(largeRingLayer.transform, current, 0, 0, 1) layer.setAffineTransform(presentation.affineTransform()) layer.removeAllAnimations() let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotateAnimation.fromValue = current rotateAnimation.toValue = current + angle rotateAnimation.duration = 5 rotateAnimation.repeatCount = .infinity layer.add(rotateAnimation, forKey: "continue") } private func pauseLayer(_ layer: CALayer) { let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil) layer.speed = 0 layer.timeOffset = pausedTime } private func resumeLayer(_ layer: CALayer) { let pausedTime = layer.timeOffset layer.speed = 1 layer.timeOffset = 0 layer.beginTime = 0 let timeSincePaused = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime layer.beginTime = timeSincePaused } // MARK: Event Response override func mouseDown(with event: NSEvent) { guard self.isEnabled else { return } if let selector = self.action { NSApp.sendAction(selector, to: self.target, from: self) } } override func updateTrackingAreas() { super.updateTrackingAreas() if let trackingArea = self.trackingArea { self.removeTrackingArea(trackingArea) } let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways] let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) self.addTrackingArea(trackingArea) } }