Files

609 lines
25 KiB
Swift

//
// 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)
}
}