|
|
@ -14,11 +14,11 @@ public final class HUDOverlayController { |
|
|
public static let shared = HUDOverlayController() |
|
|
public static let shared = HUDOverlayController() |
|
|
|
|
|
|
|
|
private var window: NSPanel? |
|
|
private var window: NSPanel? |
|
|
private var orbView: MinimalSiriOrbView? |
|
|
|
|
|
|
|
|
private var orbView: SolidSiriOrbView? |
|
|
private var isVisible = false |
|
|
private var isVisible = false |
|
|
|
|
|
|
|
|
// Transparent canvas with plenty of margin so halo never clips |
|
|
|
|
|
private let canvasSize: CGFloat = 160 |
|
|
|
|
|
|
|
|
// Large transparent canvas so audio-reactive halo never clips |
|
|
|
|
|
private let canvasSize: CGFloat = 200 |
|
|
|
|
|
|
|
|
private init() { |
|
|
private init() { |
|
|
DispatchQueue.main.async { |
|
|
DispatchQueue.main.async { |
|
|
@ -44,10 +44,10 @@ public final class HUDOverlayController { |
|
|
let container = NSView(frame: panel.contentView!.bounds) |
|
|
let container = NSView(frame: panel.contentView!.bounds) |
|
|
container.wantsLayer = true |
|
|
container.wantsLayer = true |
|
|
|
|
|
|
|
|
// Minimal Siri Orb centered in canvas |
|
|
|
|
|
let orbDiameter: CGFloat = 52 |
|
|
|
|
|
|
|
|
// Minimal Solid Orb centered in canvas |
|
|
|
|
|
let orbDiameter: CGFloat = 50 |
|
|
let orbOrigin = (canvasSize - orbDiameter) / 2.0 |
|
|
let orbOrigin = (canvasSize - orbDiameter) / 2.0 |
|
|
let orb = MinimalSiriOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbDiameter, height: orbDiameter)) |
|
|
|
|
|
|
|
|
let orb = SolidSiriOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbDiameter, height: orbDiameter)) |
|
|
container.addSubview(orb) |
|
|
container.addSubview(orb) |
|
|
self.orbView = orb |
|
|
self.orbView = orb |
|
|
|
|
|
|
|
|
@ -59,8 +59,9 @@ public final class HUDOverlayController { |
|
|
private func reposition() { |
|
|
private func reposition() { |
|
|
guard let window = self.window, let screen = NSScreen.main else { return } |
|
|
guard let window = self.window, let screen = NSScreen.main else { return } |
|
|
let screenRect = screen.visibleFrame |
|
|
let screenRect = screen.visibleFrame |
|
|
|
|
|
// Center horizontally, positioned comfortably higher above the dock |
|
|
let x = screenRect.midX - (canvasSize / 2.0) |
|
|
let x = screenRect.midX - (canvasSize / 2.0) |
|
|
let y = screenRect.minY + 28 |
|
|
|
|
|
|
|
|
let y = screenRect.minY + 75 |
|
|
window.setFrameOrigin(NSPoint(x: x, y: y)) |
|
|
window.setFrameOrigin(NSPoint(x: x, y: y)) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -126,16 +127,14 @@ public final class HUDOverlayController { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// MARK: - Minimal Clean Siri Orb (Static, Non-Rotating, Audio-Reactive Stroke & Glow) |
|
|
|
|
|
|
|
|
// MARK: - Minimal Solid-Color Orb with 100% Dynamic Audio-Driven Stroke & Bloom Halo |
|
|
|
|
|
|
|
|
final class MinimalSiriOrbView: NSView { |
|
|
|
|
|
|
|
|
final class SolidSiriOrbView: NSView { |
|
|
private let glowLayer = CALayer() |
|
|
private let glowLayer = CALayer() |
|
|
private let gradientCoreLayer = CAGradientLayer() |
|
|
|
|
|
|
|
|
private let solidCoreLayer = CALayer() |
|
|
private let strokeBorderLayer = CALayer() |
|
|
private let strokeBorderLayer = CALayer() |
|
|
|
|
|
|
|
|
private let baseDiameter: CGFloat = 52.0 |
|
|
|
|
|
private let minStrokeWidth: CGFloat = 1.5 |
|
|
|
|
|
private let maxStrokeWidth: CGFloat = 5.5 |
|
|
|
|
|
|
|
|
private let maxStrokeWidth: CGFloat = 8.0 |
|
|
|
|
|
|
|
|
override init(frame frameRect: NSRect) { |
|
|
override init(frame frameRect: NSRect) { |
|
|
super.init(frame: frameRect) |
|
|
super.init(frame: frameRect) |
|
|
@ -156,82 +155,75 @@ final class MinimalSiriOrbView: NSView { |
|
|
let bounds = self.bounds |
|
|
let bounds = self.bounds |
|
|
let radius = bounds.width / 2.0 |
|
|
let radius = bounds.width / 2.0 |
|
|
|
|
|
|
|
|
// 1. Soft Outer Atmospheric Glow (Breathes with voice volume) |
|
|
|
|
|
glowLayer.frame = bounds.insetBy(dx: -10, dy: -10) |
|
|
|
|
|
glowLayer.cornerRadius = (bounds.width + 20) / 2.0 |
|
|
|
|
|
glowLayer.backgroundColor = NSColor(red: 0.05, green: 0.65, blue: 1.0, alpha: 0.35).cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor(red: 0.50, green: 0.10, blue: 0.95, alpha: 0.85).cgColor |
|
|
|
|
|
glowLayer.shadowRadius = 14 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.60 |
|
|
|
|
|
|
|
|
// 1. Dynamic Outer Atmospheric Glow (100% audio-driven; disappears completely in silence) |
|
|
|
|
|
glowLayer.frame = bounds.insetBy(dx: -12, dy: -12) |
|
|
|
|
|
glowLayer.cornerRadius = (bounds.width + 24) / 2.0 |
|
|
|
|
|
glowLayer.backgroundColor = NSColor.clear.cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor.white.cgColor |
|
|
|
|
|
glowLayer.shadowRadius = 0 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.0 |
|
|
glowLayer.shadowOffset = .zero |
|
|
glowLayer.shadowOffset = .zero |
|
|
|
|
|
glowLayer.opacity = 0.0 |
|
|
layer.addSublayer(glowLayer) |
|
|
layer.addSublayer(glowLayer) |
|
|
|
|
|
|
|
|
// 2. Clean, Minimal, Non-Rotating Siri Gradient Sphere |
|
|
|
|
|
gradientCoreLayer.frame = bounds |
|
|
|
|
|
gradientCoreLayer.cornerRadius = radius |
|
|
|
|
|
gradientCoreLayer.type = .radial |
|
|
|
|
|
gradientCoreLayer.colors = [ |
|
|
|
|
|
NSColor(red: 0.0, green: 0.92, blue: 1.0, alpha: 1.0).cgColor, // Electric Cyan Core |
|
|
|
|
|
NSColor(red: 0.45, green: 0.12, blue: 0.96, alpha: 0.98).cgColor, // Rich Royal Violet |
|
|
|
|
|
NSColor(red: 0.90, green: 0.08, blue: 0.58, alpha: 0.92).cgColor, // Deep Magenta |
|
|
|
|
|
NSColor(red: 0.08, green: 0.10, blue: 0.35, alpha: 0.88).cgColor // Deep Navy Edge |
|
|
|
|
|
] |
|
|
|
|
|
gradientCoreLayer.startPoint = CGPoint(x: 0.40, y: 0.40) |
|
|
|
|
|
gradientCoreLayer.endPoint = CGPoint(x: 1.0, y: 1.0) |
|
|
|
|
|
layer.addSublayer(gradientCoreLayer) |
|
|
|
|
|
|
|
|
|
|
|
// 3. Dynamic Audio-Reactive Stroke Ring (Widening & brightening with speech) |
|
|
|
|
|
|
|
|
// 2. Chic, Minimal Solid-Color Core (Apple Obsidian Graphite Disc) |
|
|
|
|
|
solidCoreLayer.frame = bounds |
|
|
|
|
|
solidCoreLayer.cornerRadius = radius |
|
|
|
|
|
solidCoreLayer.backgroundColor = NSColor(red: 0.12, green: 0.12, blue: 0.14, alpha: 0.98).cgColor |
|
|
|
|
|
layer.addSublayer(solidCoreLayer) |
|
|
|
|
|
|
|
|
|
|
|
// 3. Dynamic Voice-Reactive Stroke Ring (Disappears completely in silence) |
|
|
strokeBorderLayer.frame = bounds |
|
|
strokeBorderLayer.frame = bounds |
|
|
strokeBorderLayer.cornerRadius = radius |
|
|
strokeBorderLayer.cornerRadius = radius |
|
|
strokeBorderLayer.borderWidth = minStrokeWidth |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor.white.withAlphaComponent(0.35).cgColor |
|
|
|
|
|
|
|
|
strokeBorderLayer.borderWidth = 0.0 |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor.white.cgColor |
|
|
strokeBorderLayer.backgroundColor = NSColor.clear.cgColor |
|
|
strokeBorderLayer.backgroundColor = NSColor.clear.cgColor |
|
|
|
|
|
strokeBorderLayer.opacity = 0.0 |
|
|
layer.addSublayer(strokeBorderLayer) |
|
|
layer.addSublayer(strokeBorderLayer) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// Updates audio response: The orb remains steady (no jiggling/scaling), |
|
|
|
|
|
/// while the stroke border thickness and outer glow bloom dynamically with voice. |
|
|
|
|
|
|
|
|
/// Updates audio response: |
|
|
|
|
|
/// In silence (level <= 0.04): stroke and halo disappear completely. |
|
|
|
|
|
/// When speaking: stroke widens powerfully (up to 8px) and halo blooms dynamically. |
|
|
func updateAudioLevel(_ level: Float, isLagging: Bool) { |
|
|
func updateAudioLevel(_ level: Float, isLagging: Bool) { |
|
|
CATransaction.begin() |
|
|
CATransaction.begin() |
|
|
CATransaction.setDisableActions(true) |
|
|
CATransaction.setDisableActions(true) |
|
|
|
|
|
|
|
|
let clamped = CGFloat(min(max(level, 0.0), 1.0)) |
|
|
|
|
|
|
|
|
let rawClamped = CGFloat(min(max(level, 0.0), 1.0)) |
|
|
|
|
|
let silenceThreshold: CGFloat = 0.04 |
|
|
|
|
|
|
|
|
|
|
|
if rawClamped <= silenceThreshold { |
|
|
|
|
|
// Complete silence: Stroke and Halo completely vanish |
|
|
|
|
|
strokeBorderLayer.borderWidth = 0.0 |
|
|
|
|
|
strokeBorderLayer.opacity = 0.0 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.0 |
|
|
|
|
|
glowLayer.shadowRadius = 0.0 |
|
|
|
|
|
glowLayer.opacity = 0.0 |
|
|
|
|
|
} else { |
|
|
|
|
|
// Active speech: scale dynamic intensity noticeably |
|
|
|
|
|
let norm = (rawClamped - silenceThreshold) / (1.0 - silenceThreshold) |
|
|
|
|
|
|
|
|
if isLagging { |
|
|
if isLagging { |
|
|
// Warm amber indicator for network degradation |
|
|
|
|
|
gradientCoreLayer.colors = [ |
|
|
|
|
|
NSColor(red: 1.0, green: 0.80, blue: 0.20, alpha: 1.0).cgColor, |
|
|
|
|
|
NSColor(red: 1.0, green: 0.45, blue: 0.0, alpha: 0.95).cgColor, |
|
|
|
|
|
NSColor(red: 0.85, green: 0.15, blue: 0.0, alpha: 0.90).cgColor |
|
|
|
|
|
] |
|
|
|
|
|
let strokeColor = NSColor(red: 1.0, green: 0.70, blue: 0.20, alpha: 0.6 + (0.4 * clamped)) |
|
|
|
|
|
strokeBorderLayer.borderColor = strokeColor.cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor.orange.cgColor |
|
|
|
|
|
|
|
|
// Amber highlight for network degradation |
|
|
|
|
|
let amber = NSColor(red: 1.0, green: 0.65, blue: 0.10, alpha: 0.95).cgColor |
|
|
|
|
|
strokeBorderLayer.borderColor = amber |
|
|
|
|
|
glowLayer.shadowColor = amber |
|
|
} else { |
|
|
} else { |
|
|
// Standard crisp Siri palette |
|
|
|
|
|
gradientCoreLayer.colors = [ |
|
|
|
|
|
NSColor(red: 0.0, green: 0.92, blue: 1.0, alpha: 1.0).cgColor, |
|
|
|
|
|
NSColor(red: 0.45, green: 0.12, blue: 0.96, alpha: 0.98).cgColor, |
|
|
|
|
|
NSColor(red: 0.90, green: 0.08, blue: 0.58, alpha: 0.92).cgColor, |
|
|
|
|
|
NSColor(red: 0.08, green: 0.10, blue: 0.35, alpha: 0.88).cgColor |
|
|
|
|
|
] |
|
|
|
|
|
let strokeColor = NSColor( |
|
|
|
|
|
red: 0.3 + (0.7 * clamped), |
|
|
|
|
|
green: 0.95, |
|
|
|
|
|
blue: 1.0, |
|
|
|
|
|
alpha: 0.35 + (0.65 * clamped) |
|
|
|
|
|
) |
|
|
|
|
|
strokeBorderLayer.borderColor = strokeColor.cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor(red: 0.45, green: 0.10, blue: 0.95, alpha: 0.85).cgColor |
|
|
|
|
|
|
|
|
// Crisp luminous white glow |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor.white.cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor.white.cgColor |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 1. Dynamic Stroke Width: from 1.5px up to 5.5px based on voice volume |
|
|
|
|
|
let dynamicWidth = minStrokeWidth + (clamped * (maxStrokeWidth - minStrokeWidth)) |
|
|
|
|
|
|
|
|
// 1. Noticeably powerful stroke expansion (from 0 to 8px) |
|
|
|
|
|
let dynamicWidth = norm * maxStrokeWidth |
|
|
strokeBorderLayer.borderWidth = dynamicWidth |
|
|
strokeBorderLayer.borderWidth = dynamicWidth |
|
|
|
|
|
strokeBorderLayer.opacity = Float(0.40 + (0.60 * norm)) |
|
|
|
|
|
|
|
|
// 2. Dynamic Outer Glow: blooms softly with speech |
|
|
|
|
|
glowLayer.shadowRadius = 14 + (clamped * 24) |
|
|
|
|
|
glowLayer.shadowOpacity = Float(0.60 + (clamped * 0.35)) |
|
|
|
|
|
|
|
|
// 2. Dynamic bloom halo (expands up to 45px radius) |
|
|
|
|
|
glowLayer.shadowRadius = 6.0 + (norm * 38.0) |
|
|
|
|
|
glowLayer.shadowOpacity = Float(0.35 + (0.65 * norm)) |
|
|
|
|
|
glowLayer.opacity = Float(norm) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
CATransaction.commit() |
|
|
CATransaction.commit() |
|
|
} |
|
|
} |
|
|
@ -239,20 +231,27 @@ final class MinimalSiriOrbView: NSView { |
|
|
func showRetryState() { |
|
|
func showRetryState() { |
|
|
CATransaction.begin() |
|
|
CATransaction.begin() |
|
|
CATransaction.setDisableActions(false) |
|
|
CATransaction.setDisableActions(false) |
|
|
CATransaction.setAnimationDuration(0.25) |
|
|
|
|
|
strokeBorderLayer.borderWidth = 3.5 |
|
|
|
|
|
|
|
|
CATransaction.setAnimationDuration(0.20) |
|
|
|
|
|
strokeBorderLayer.borderWidth = 4.0 |
|
|
|
|
|
strokeBorderLayer.opacity = 1.0 |
|
|
strokeBorderLayer.borderColor = NSColor.orange.cgColor |
|
|
strokeBorderLayer.borderColor = NSColor.orange.cgColor |
|
|
glowLayer.shadowColor = NSColor.orange.cgColor |
|
|
glowLayer.shadowColor = NSColor.orange.cgColor |
|
|
glowLayer.shadowRadius = 22 |
|
|
|
|
|
|
|
|
glowLayer.shadowRadius = 24 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.9 |
|
|
|
|
|
glowLayer.opacity = 1.0 |
|
|
CATransaction.commit() |
|
|
CATransaction.commit() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func showTranscribingState() { |
|
|
func showTranscribingState() { |
|
|
CATransaction.begin() |
|
|
CATransaction.begin() |
|
|
CATransaction.setDisableActions(true) |
|
|
CATransaction.setDisableActions(true) |
|
|
strokeBorderLayer.borderWidth = 2.5 |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor.cyan.cgColor |
|
|
|
|
|
glowLayer.shadowRadius = 18 |
|
|
|
|
|
|
|
|
strokeBorderLayer.borderWidth = 3.0 |
|
|
|
|
|
strokeBorderLayer.opacity = 0.8 |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor.white.cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor.white.cgColor |
|
|
|
|
|
glowLayer.shadowRadius = 16 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.7 |
|
|
|
|
|
glowLayer.opacity = 0.8 |
|
|
CATransaction.commit() |
|
|
CATransaction.commit() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -260,10 +259,14 @@ final class MinimalSiriOrbView: NSView { |
|
|
CATransaction.begin() |
|
|
CATransaction.begin() |
|
|
CATransaction.setDisableActions(false) |
|
|
CATransaction.setDisableActions(false) |
|
|
CATransaction.setAnimationDuration(0.18) |
|
|
CATransaction.setAnimationDuration(0.18) |
|
|
strokeBorderLayer.borderWidth = 3.0 |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor(red: 0.1, green: 0.95, blue: 0.5, alpha: 1.0).cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor.green.cgColor |
|
|
|
|
|
glowLayer.shadowRadius = 20 |
|
|
|
|
|
|
|
|
let green = NSColor(red: 0.15, green: 0.95, blue: 0.45, alpha: 1.0).cgColor |
|
|
|
|
|
strokeBorderLayer.borderWidth = 4.0 |
|
|
|
|
|
strokeBorderLayer.opacity = 1.0 |
|
|
|
|
|
strokeBorderLayer.borderColor = green |
|
|
|
|
|
glowLayer.shadowColor = green |
|
|
|
|
|
glowLayer.shadowRadius = 22 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.9 |
|
|
|
|
|
glowLayer.opacity = 1.0 |
|
|
CATransaction.commit() |
|
|
CATransaction.commit() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -271,20 +274,25 @@ final class MinimalSiriOrbView: NSView { |
|
|
CATransaction.begin() |
|
|
CATransaction.begin() |
|
|
CATransaction.setDisableActions(false) |
|
|
CATransaction.setDisableActions(false) |
|
|
CATransaction.setAnimationDuration(0.18) |
|
|
CATransaction.setAnimationDuration(0.18) |
|
|
strokeBorderLayer.borderWidth = 3.0 |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor.red.cgColor |
|
|
|
|
|
glowLayer.shadowColor = NSColor.red.cgColor |
|
|
|
|
|
glowLayer.shadowRadius = 20 |
|
|
|
|
|
|
|
|
let red = NSColor(red: 1.0, green: 0.20, blue: 0.20, alpha: 1.0).cgColor |
|
|
|
|
|
strokeBorderLayer.borderWidth = 4.0 |
|
|
|
|
|
strokeBorderLayer.opacity = 1.0 |
|
|
|
|
|
strokeBorderLayer.borderColor = red |
|
|
|
|
|
glowLayer.shadowColor = red |
|
|
|
|
|
glowLayer.shadowRadius = 22 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.9 |
|
|
|
|
|
glowLayer.opacity = 1.0 |
|
|
CATransaction.commit() |
|
|
CATransaction.commit() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func reset() { |
|
|
func reset() { |
|
|
CATransaction.begin() |
|
|
CATransaction.begin() |
|
|
CATransaction.setDisableActions(true) |
|
|
CATransaction.setDisableActions(true) |
|
|
strokeBorderLayer.borderWidth = minStrokeWidth |
|
|
|
|
|
strokeBorderLayer.borderColor = NSColor.white.withAlphaComponent(0.35).cgColor |
|
|
|
|
|
glowLayer.shadowRadius = 14 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.60 |
|
|
|
|
|
|
|
|
strokeBorderLayer.borderWidth = 0.0 |
|
|
|
|
|
strokeBorderLayer.opacity = 0.0 |
|
|
|
|
|
glowLayer.shadowRadius = 0.0 |
|
|
|
|
|
glowLayer.shadowOpacity = 0.0 |
|
|
|
|
|
glowLayer.opacity = 0.0 |
|
|
CATransaction.commit() |
|
|
CATransaction.commit() |
|
|
} |
|
|
} |
|
|
} |
|
|
} |