Sin descripción

LoadingView.swift 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. //
  2. // LoadingView.swift
  3. // App for Indeed
  4. //
  5. import Cocoa
  6. import QuartzCore
  7. final class LoadingView: NSView {
  8. private enum Theme {
  9. static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
  10. static var brandBlueLight: NSColor { AppDashboardTheme.brandBlueHover }
  11. static var pageBackgroundTop: NSColor { AppDashboardTheme.loadingPageBackgroundTop }
  12. static var pageBackgroundBottom: NSColor { AppDashboardTheme.loadingPageBackgroundBottom }
  13. static var iconWell: NSColor { AppDashboardTheme.loadingIconWell }
  14. static var headingBlue: NSColor { AppDashboardTheme.loadingHeadingBlue }
  15. static var subtitleText: NSColor { AppDashboardTheme.loadingSubtitleText }
  16. static var statusText: NSColor { AppDashboardTheme.loadingStatusText }
  17. static var waveTint: NSColor { AppDashboardTheme.loadingWaveTint }
  18. static var badgeFill: NSColor { AppDashboardTheme.loadingBadgeFill }
  19. static var badgeText: NSColor { AppDashboardTheme.loadingBadgeText }
  20. static var iconWellBorder: NSColor { AppDashboardTheme.loadingIconWellBorder }
  21. }
  22. private var appearanceObserver: NSObjectProtocol?
  23. private let backgroundGradientHost = NSView()
  24. private let heroBackground = LoadingSplashBackgroundView()
  25. private let iconWell = NSView()
  26. private let logoView = IndeedLogoView(displayHeight: 40, variant: .compact)
  27. private let aiBadgeHost = NSView()
  28. private let aiBadgeLabel = NSTextField(labelWithString: L("AI-POWERED"))
  29. private let titleLabel = NSTextField(labelWithString: AppMarketingLinks.displayName)
  30. private let subtitleLabel = NSTextField(labelWithString: L("Find your perfect job with the power of AI."))
  31. private let statusLabel = NSTextField(labelWithString: L("Starting up…"))
  32. private let progressBar = LoadingProgressBarView()
  33. private let thinkingIndicator = ChatThinkingIndicatorView(
  34. compact: false,
  35. accessibilityLabel: String(format: L("Loading %@"), AppMarketingLinks.displayName)
  36. )
  37. override init(frame frameRect: NSRect) {
  38. super.init(frame: frameRect)
  39. setUp()
  40. appearanceObserver = NotificationCenter.default.addObserver(
  41. forName: AppAppearanceManager.didChangeNotification,
  42. object: nil,
  43. queue: .main
  44. ) { [weak self] _ in
  45. self?.applyCurrentAppearance()
  46. }
  47. }
  48. deinit {
  49. if let appearanceObserver {
  50. NotificationCenter.default.removeObserver(appearanceObserver)
  51. }
  52. }
  53. @available(*, unavailable)
  54. required init?(coder: NSCoder) {
  55. fatalError("init(coder:) has not been implemented")
  56. }
  57. override func viewDidChangeEffectiveAppearance() {
  58. super.viewDidChangeEffectiveAppearance()
  59. applyCurrentAppearance()
  60. }
  61. override func layout() {
  62. super.layout()
  63. backgroundGradientHost.layer?.sublayers?
  64. .filter { $0.name == "pageGradient" }
  65. .forEach { $0.frame = backgroundGradientHost.bounds }
  66. progressBar.layoutProgressFill(animated: false)
  67. }
  68. override func viewDidMoveToWindow() {
  69. super.viewDidMoveToWindow()
  70. if window != nil {
  71. startAnimating()
  72. } else {
  73. stopAnimating()
  74. }
  75. }
  76. func setStatus(_ message: String, progress: CGFloat) {
  77. statusLabel.stringValue = message
  78. progressBar.setProgress(progress, animated: true)
  79. setAccessibilityLabel(String(format: L("Loading %@. %@"), AppMarketingLinks.displayName, message))
  80. }
  81. func startAnimating() {
  82. thinkingIndicator.startAnimatingIfNeeded()
  83. progressBar.startShimmerIfNeeded()
  84. heroBackground.startAmbientAnimationIfNeeded()
  85. }
  86. func stopAnimating() {
  87. thinkingIndicator.stopAnimating()
  88. progressBar.stopShimmer()
  89. heroBackground.stopAmbientAnimation()
  90. }
  91. private func setUp() {
  92. wantsLayer = true
  93. backgroundGradientHost.translatesAutoresizingMaskIntoConstraints = false
  94. backgroundGradientHost.wantsLayer = true
  95. heroBackground.translatesAutoresizingMaskIntoConstraints = false
  96. heroBackground.waveTint = Theme.waveTint
  97. iconWell.translatesAutoresizingMaskIntoConstraints = false
  98. iconWell.wantsLayer = true
  99. iconWell.layer?.backgroundColor = Theme.iconWell.cgColor
  100. iconWell.layer?.cornerRadius = 28
  101. if #available(macOS 11.0, *) {
  102. iconWell.layer?.cornerCurve = .continuous
  103. }
  104. iconWell.layer?.masksToBounds = true
  105. iconWell.layer?.borderColor = Theme.iconWellBorder.cgColor
  106. iconWell.layer?.borderWidth = 3
  107. iconWell.layer?.shadowColor = NSColor.black.cgColor
  108. iconWell.layer?.shadowOpacity = 0.08
  109. iconWell.layer?.shadowRadius = 16
  110. iconWell.layer?.shadowOffset = CGSize(width: 0, height: -4)
  111. logoView.translatesAutoresizingMaskIntoConstraints = false
  112. aiBadgeHost.translatesAutoresizingMaskIntoConstraints = false
  113. aiBadgeHost.wantsLayer = true
  114. aiBadgeHost.layer?.backgroundColor = Theme.badgeFill.cgColor
  115. aiBadgeHost.layer?.cornerRadius = 10
  116. if #available(macOS 11.0, *) {
  117. aiBadgeHost.layer?.cornerCurve = .continuous
  118. }
  119. aiBadgeLabel.translatesAutoresizingMaskIntoConstraints = false
  120. aiBadgeLabel.font = .systemFont(ofSize: 11, weight: .bold)
  121. aiBadgeLabel.textColor = Theme.badgeText
  122. aiBadgeLabel.alignment = .center
  123. aiBadgeLabel.isEditable = false
  124. aiBadgeLabel.isSelectable = false
  125. aiBadgeLabel.isBezeled = false
  126. aiBadgeLabel.drawsBackground = false
  127. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  128. titleLabel.font = .systemFont(ofSize: 44, weight: .bold)
  129. titleLabel.textColor = Theme.headingBlue
  130. titleLabel.alignment = .center
  131. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  132. subtitleLabel.font = .systemFont(ofSize: 18, weight: .regular)
  133. subtitleLabel.textColor = Theme.subtitleText
  134. subtitleLabel.alignment = .center
  135. subtitleLabel.maximumNumberOfLines = 2
  136. subtitleLabel.lineBreakMode = .byWordWrapping
  137. statusLabel.translatesAutoresizingMaskIntoConstraints = false
  138. statusLabel.font = .systemFont(ofSize: 15, weight: .medium)
  139. statusLabel.textColor = Theme.statusText
  140. statusLabel.alignment = .center
  141. statusLabel.maximumNumberOfLines = 2
  142. statusLabel.lineBreakMode = .byTruncatingTail
  143. progressBar.translatesAutoresizingMaskIntoConstraints = false
  144. thinkingIndicator.translatesAutoresizingMaskIntoConstraints = false
  145. iconWell.addSubview(logoView)
  146. aiBadgeHost.addSubview(aiBadgeLabel)
  147. let heroStack = NSStackView(views: [
  148. iconWell,
  149. aiBadgeHost,
  150. titleLabel,
  151. subtitleLabel,
  152. statusLabel,
  153. thinkingIndicator,
  154. progressBar
  155. ])
  156. heroStack.orientation = .vertical
  157. heroStack.alignment = .centerX
  158. heroStack.spacing = 18
  159. heroStack.setCustomSpacing(28, after: iconWell)
  160. heroStack.setCustomSpacing(12, after: aiBadgeHost)
  161. heroStack.setCustomSpacing(10, after: titleLabel)
  162. heroStack.setCustomSpacing(28, after: subtitleLabel)
  163. heroStack.setCustomSpacing(24, after: statusLabel)
  164. heroStack.setCustomSpacing(24, after: thinkingIndicator)
  165. heroStack.translatesAutoresizingMaskIntoConstraints = false
  166. addSubview(backgroundGradientHost)
  167. addSubview(heroBackground)
  168. addSubview(heroStack)
  169. NSLayoutConstraint.activate([
  170. backgroundGradientHost.leadingAnchor.constraint(equalTo: leadingAnchor),
  171. backgroundGradientHost.trailingAnchor.constraint(equalTo: trailingAnchor),
  172. backgroundGradientHost.topAnchor.constraint(equalTo: topAnchor),
  173. backgroundGradientHost.bottomAnchor.constraint(equalTo: bottomAnchor),
  174. heroBackground.leadingAnchor.constraint(equalTo: leadingAnchor),
  175. heroBackground.trailingAnchor.constraint(equalTo: trailingAnchor),
  176. heroBackground.topAnchor.constraint(equalTo: topAnchor),
  177. heroBackground.bottomAnchor.constraint(equalTo: bottomAnchor),
  178. iconWell.widthAnchor.constraint(greaterThanOrEqualToConstant: 160),
  179. iconWell.heightAnchor.constraint(equalToConstant: 80),
  180. logoView.centerXAnchor.constraint(equalTo: iconWell.centerXAnchor),
  181. logoView.centerYAnchor.constraint(equalTo: iconWell.centerYAnchor),
  182. logoView.leadingAnchor.constraint(greaterThanOrEqualTo: iconWell.leadingAnchor, constant: 24),
  183. logoView.trailingAnchor.constraint(lessThanOrEqualTo: iconWell.trailingAnchor, constant: -24),
  184. aiBadgeHost.heightAnchor.constraint(equalToConstant: 26),
  185. aiBadgeLabel.leadingAnchor.constraint(equalTo: aiBadgeHost.leadingAnchor, constant: 14),
  186. aiBadgeLabel.trailingAnchor.constraint(equalTo: aiBadgeHost.trailingAnchor, constant: -14),
  187. aiBadgeLabel.centerYAnchor.constraint(equalTo: aiBadgeHost.centerYAnchor),
  188. heroStack.centerXAnchor.constraint(equalTo: centerXAnchor),
  189. heroStack.centerYAnchor.constraint(equalTo: centerYAnchor),
  190. heroStack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 48),
  191. heroStack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -48),
  192. heroStack.widthAnchor.constraint(lessThanOrEqualToConstant: 720),
  193. subtitleLabel.widthAnchor.constraint(lessThanOrEqualTo: heroStack.widthAnchor),
  194. statusLabel.widthAnchor.constraint(lessThanOrEqualTo: heroStack.widthAnchor),
  195. progressBar.widthAnchor.constraint(equalTo: heroStack.widthAnchor, multiplier: 0.78),
  196. progressBar.widthAnchor.constraint(lessThanOrEqualToConstant: 560),
  197. progressBar.widthAnchor.constraint(greaterThanOrEqualToConstant: 280)
  198. ])
  199. installPageGradient()
  200. setAccessibilityElement(true)
  201. setAccessibilityRole(.group)
  202. setAccessibilityLabel(String(format: L("Loading %@"), AppMarketingLinks.displayName))
  203. }
  204. private func installPageGradient() {
  205. backgroundGradientHost.layer?.sublayers?
  206. .filter { $0.name == "pageGradient" }
  207. .forEach { $0.removeFromSuperlayer() }
  208. let gradient = CAGradientLayer()
  209. gradient.name = "pageGradient"
  210. gradient.colors = [
  211. Theme.pageBackgroundTop.cgColor,
  212. Theme.pageBackgroundBottom.cgColor
  213. ]
  214. gradient.startPoint = CGPoint(x: 0.5, y: 1)
  215. gradient.endPoint = CGPoint(x: 0.5, y: 0)
  216. gradient.frame = backgroundGradientHost.bounds
  217. backgroundGradientHost.layer?.addSublayer(gradient)
  218. }
  219. private func applyCurrentAppearance() {
  220. window?.backgroundColor = AppAppearanceManager.shared.windowChromeColor
  221. heroBackground.waveTint = Theme.waveTint
  222. iconWell.layer?.backgroundColor = Theme.iconWell.cgColor
  223. iconWell.layer?.borderColor = Theme.iconWellBorder.cgColor
  224. aiBadgeHost.layer?.backgroundColor = Theme.badgeFill.cgColor
  225. aiBadgeLabel.textColor = Theme.badgeText
  226. titleLabel.textColor = Theme.headingBlue
  227. subtitleLabel.textColor = Theme.subtitleText
  228. statusLabel.textColor = Theme.statusText
  229. installPageGradient()
  230. progressBar.applyCurrentAppearance()
  231. }
  232. }
  233. // MARK: - Loading progress bar
  234. private final class LoadingProgressBarView: NSView {
  235. private enum Theme {
  236. static var brandBlue: NSColor { AppDashboardTheme.brandBlue }
  237. static var brandBlueLight: NSColor { AppDashboardTheme.brandBlueHover }
  238. static var trackFill: NSColor { AppDashboardTheme.loadingTrackFill }
  239. static var trackBorder: NSColor { AppDashboardTheme.loadingTrackBorder }
  240. static var percentText: NSColor { AppDashboardTheme.brandBlue }
  241. }
  242. private let track = NSView()
  243. private let fill = NSView()
  244. private let percentLabel = NSTextField(labelWithString: "0%")
  245. private var fillWidthConstraint: NSLayoutConstraint?
  246. private var fillGradientLayer: CAGradientLayer?
  247. private var shimmerLayer: CAGradientLayer?
  248. private var targetProgress: CGFloat = 0.04
  249. override var intrinsicContentSize: NSSize {
  250. NSSize(width: 420, height: 44)
  251. }
  252. override init(frame frameRect: NSRect) {
  253. super.init(frame: frameRect)
  254. setUp()
  255. }
  256. @available(*, unavailable)
  257. required init?(coder: NSCoder) {
  258. fatalError("init(coder:) has not been implemented")
  259. }
  260. override func layout() {
  261. super.layout()
  262. layoutProgressFill(animated: false)
  263. }
  264. func setProgress(_ progress: CGFloat, animated: Bool) {
  265. targetProgress = min(max(progress, 0), 1)
  266. let percent = Int((targetProgress * 100).rounded())
  267. percentLabel.stringValue = "\(percent)%"
  268. layoutProgressFill(animated: animated)
  269. }
  270. func layoutProgressFill(animated: Bool) {
  271. layoutSubtreeIfNeeded()
  272. let trackWidth = track.bounds.width
  273. guard trackWidth > 1 else { return }
  274. let width = max(trackWidth * max(targetProgress, 0.03), 12)
  275. fillWidthConstraint?.constant = width
  276. guard animated else {
  277. fillGradientLayer?.frame = fill.bounds
  278. shimmerLayer?.frame = fill.bounds
  279. return
  280. }
  281. NSAnimationContext.runAnimationGroup { context in
  282. context.duration = 0.45
  283. context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
  284. context.allowsImplicitAnimation = true
  285. fill.animator().layoutSubtreeIfNeeded()
  286. } completionHandler: { [weak self] in
  287. guard let self else { return }
  288. self.fillGradientLayer?.frame = self.fill.bounds
  289. self.shimmerLayer?.frame = self.fill.bounds
  290. }
  291. }
  292. func startShimmerIfNeeded() {
  293. guard !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion,
  294. let shimmerLayer else { return }
  295. shimmerLayer.removeAnimation(forKey: "shimmer")
  296. let move = CABasicAnimation(keyPath: "transform.translation.x")
  297. move.fromValue = -fill.bounds.width * 0.6
  298. move.toValue = fill.bounds.width * 0.6
  299. move.duration = 1.35
  300. move.repeatCount = .greatestFiniteMagnitude
  301. move.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
  302. shimmerLayer.add(move, forKey: "shimmer")
  303. }
  304. func stopShimmer() {
  305. shimmerLayer?.removeAnimation(forKey: "shimmer")
  306. }
  307. func applyCurrentAppearance() {
  308. track.layer?.backgroundColor = Theme.trackFill.cgColor
  309. track.layer?.borderColor = Theme.trackBorder.cgColor
  310. percentLabel.textColor = Theme.percentText
  311. fillGradientLayer?.colors = [
  312. Theme.brandBlueLight.cgColor,
  313. Theme.brandBlue.cgColor
  314. ]
  315. }
  316. private func setUp() {
  317. translatesAutoresizingMaskIntoConstraints = false
  318. track.translatesAutoresizingMaskIntoConstraints = false
  319. track.wantsLayer = true
  320. track.layer?.backgroundColor = Theme.trackFill.cgColor
  321. track.layer?.borderColor = Theme.trackBorder.cgColor
  322. track.layer?.borderWidth = 1
  323. track.layer?.cornerRadius = 7
  324. if #available(macOS 11.0, *) {
  325. track.layer?.cornerCurve = .continuous
  326. }
  327. track.layer?.masksToBounds = true
  328. track.setContentHuggingPriority(.defaultLow, for: .horizontal)
  329. track.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  330. fill.translatesAutoresizingMaskIntoConstraints = false
  331. fill.wantsLayer = true
  332. fill.layer?.cornerRadius = 7
  333. if #available(macOS 11.0, *) {
  334. fill.layer?.cornerCurve = .continuous
  335. }
  336. fill.layer?.masksToBounds = true
  337. let fillGradient = CAGradientLayer()
  338. fillGradient.colors = [
  339. Theme.brandBlueLight.cgColor,
  340. Theme.brandBlue.cgColor
  341. ]
  342. fillGradient.startPoint = CGPoint(x: 0, y: 0.5)
  343. fillGradient.endPoint = CGPoint(x: 1, y: 0.5)
  344. fillGradient.cornerRadius = 7
  345. fill.layer?.addSublayer(fillGradient)
  346. fillGradientLayer = fillGradient
  347. let shimmer = CAGradientLayer()
  348. shimmer.colors = [
  349. NSColor.white.withAlphaComponent(0).cgColor,
  350. NSColor.white.withAlphaComponent(0.45).cgColor,
  351. NSColor.white.withAlphaComponent(0).cgColor
  352. ]
  353. shimmer.startPoint = CGPoint(x: 0, y: 0.5)
  354. shimmer.endPoint = CGPoint(x: 1, y: 0.5)
  355. shimmer.locations = [0, 0.5, 1]
  356. fill.layer?.addSublayer(shimmer)
  357. shimmerLayer = shimmer
  358. percentLabel.translatesAutoresizingMaskIntoConstraints = false
  359. percentLabel.font = .monospacedDigitSystemFont(ofSize: 13, weight: .semibold)
  360. percentLabel.textColor = Theme.percentText
  361. percentLabel.alignment = .right
  362. percentLabel.setContentHuggingPriority(.required, for: .horizontal)
  363. track.addSubview(fill)
  364. let row = NSStackView(views: [track, percentLabel])
  365. row.orientation = .horizontal
  366. row.spacing = 14
  367. row.alignment = .centerY
  368. row.translatesAutoresizingMaskIntoConstraints = false
  369. addSubview(row)
  370. fillWidthConstraint = fill.widthAnchor.constraint(equalToConstant: 12)
  371. fillWidthConstraint?.isActive = true
  372. NSLayoutConstraint.activate([
  373. row.leadingAnchor.constraint(equalTo: leadingAnchor),
  374. row.trailingAnchor.constraint(equalTo: trailingAnchor),
  375. row.topAnchor.constraint(equalTo: topAnchor),
  376. row.bottomAnchor.constraint(equalTo: bottomAnchor),
  377. track.heightAnchor.constraint(equalToConstant: 14),
  378. fill.leadingAnchor.constraint(equalTo: track.leadingAnchor),
  379. fill.topAnchor.constraint(equalTo: track.topAnchor),
  380. fill.bottomAnchor.constraint(equalTo: track.bottomAnchor),
  381. percentLabel.widthAnchor.constraint(equalToConstant: 44)
  382. ])
  383. setAccessibilityElement(true)
  384. setAccessibilityRole(.progressIndicator)
  385. setAccessibilityLabel(L("Loading progress"))
  386. }
  387. }
  388. // MARK: - Full-page decorative background (matches dashboard welcome hero)
  389. private final class LoadingSplashBackgroundView: NSView {
  390. var waveTint = NSColor(srgbRed: 186 / 255, green: 210 / 255, blue: 253 / 255, alpha: 1)
  391. override var isFlipped: Bool { true }
  392. override func draw(_ dirtyRect: NSRect) {
  393. NSColor.clear.setFill()
  394. bounds.fill()
  395. guard bounds.width > 24, bounds.height > 24 else { return }
  396. drawSideWaves(in: bounds, isLeft: true)
  397. drawSideWaves(in: bounds, isLeft: false)
  398. drawAmbientSparkles(in: bounds)
  399. }
  400. func startAmbientAnimationIfNeeded() {
  401. guard !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion else { return }
  402. let anim = CABasicAnimation(keyPath: "opacity")
  403. anim.fromValue = 0.92
  404. anim.toValue = 1
  405. anim.duration = 2.4
  406. anim.autoreverses = true
  407. anim.repeatCount = .greatestFiniteMagnitude
  408. layer?.add(anim, forKey: "ambientPulse")
  409. }
  410. func stopAmbientAnimation() {
  411. layer?.removeAnimation(forKey: "ambientPulse")
  412. }
  413. private func drawSideWaves(in bounds: NSRect, isLeft: Bool) {
  414. for i in 0..<12 {
  415. let path = NSBezierPath()
  416. path.lineWidth = 1.2
  417. path.lineCapStyle = .round
  418. let phase = CGFloat(i) * 0.88
  419. let base = CGFloat(i + 1) * 14 + 8
  420. var first = true
  421. for y in stride(from: CGFloat(0), through: bounds.height, by: 2.4) {
  422. let wobble = sin(y * 0.042 + phase) * (5 + CGFloat(i % 5))
  423. let x = isLeft ? (base + wobble) : (bounds.width - base - wobble)
  424. let point = NSPoint(x: x, y: y)
  425. if first {
  426. path.move(to: point)
  427. first = false
  428. } else {
  429. path.line(to: point)
  430. }
  431. }
  432. let fade = 1 - CGFloat(i) / 13
  433. waveTint.withAlphaComponent((0.1 + CGFloat(i % 3) * 0.024) * fade).setStroke()
  434. path.stroke()
  435. }
  436. }
  437. private func drawAmbientSparkles(in bounds: NSRect) {
  438. let accent = NSColor(srgbRed: 0, green: 82 / 255, blue: 204 / 255, alpha: 1)
  439. let specs: [(CGFloat, CGFloat, CGFloat, CGFloat)] = [
  440. (0.06, 0.1, 12, 0.32),
  441. (0.94, 0.08, 14, 0.36),
  442. (0.12, 0.38, 7, 0.22),
  443. (0.88, 0.36, 8, 0.24),
  444. (0.5, 0.05, 10, 0.18),
  445. (0.22, 0.72, 6, 0.16),
  446. (0.78, 0.68, 9, 0.2),
  447. (0.04, 0.55, 5, 0.14),
  448. (0.96, 0.52, 6, 0.15)
  449. ]
  450. for (nx, ny, size, a) in specs {
  451. let center = NSPoint(x: bounds.width * nx, y: bounds.height * ny)
  452. fillFourPointStar(center: center, radius: size, color: accent.withAlphaComponent(a))
  453. }
  454. }
  455. private func fillFourPointStar(center: NSPoint, radius: CGFloat, color: NSColor) {
  456. let path = NSBezierPath()
  457. for i in 0..<4 {
  458. let angle = CGFloat(i) * .pi / 2 - .pi / 2
  459. let point = NSPoint(
  460. x: center.x + cos(angle) * radius,
  461. y: center.y + sin(angle) * radius
  462. )
  463. if i == 0 {
  464. path.move(to: point)
  465. } else {
  466. path.line(to: point)
  467. }
  468. }
  469. path.close()
  470. color.setFill()
  471. path.fill()
  472. }
  473. }