You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
13 KiB
336 lines
13 KiB
import Cocoa
|
|
import QuartzCore
|
|
|
|
public enum HUDState {
|
|
case hidden
|
|
case recording(level: Float, isLagging: Bool = false)
|
|
case retrying
|
|
case transcribing
|
|
case success
|
|
case error
|
|
}
|
|
|
|
public final class HUDOverlayController {
|
|
public static let shared = HUDOverlayController()
|
|
|
|
private var window: NSPanel?
|
|
private var orbView: SiriOrbView?
|
|
private var isVisible = false
|
|
|
|
// Large canvas so shadows/halos NEVER clip at window boundaries
|
|
private let canvasSize: CGFloat = 260
|
|
|
|
private init() {
|
|
DispatchQueue.main.async {
|
|
self.setupWindow()
|
|
}
|
|
}
|
|
|
|
private func setupWindow() {
|
|
let panel = NSPanel(
|
|
contentRect: NSRect(x: 0, y: 0, width: canvasSize, height: canvasSize),
|
|
styleMask: [.borderless, .nonactivatingPanel],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
panel.isOpaque = false
|
|
panel.backgroundColor = .clear
|
|
panel.level = .floating
|
|
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
|
|
panel.hasShadow = false
|
|
panel.ignoresMouseEvents = true
|
|
panel.alphaValue = 0.0
|
|
|
|
let container = NSView(frame: panel.contentView!.bounds)
|
|
container.wantsLayer = true
|
|
|
|
// Siri-like Circular Orb centered with plenty of breathing room for glowing halos
|
|
let orbSize: CGFloat = 64
|
|
let orbOrigin = (canvasSize - orbSize) / 2.0
|
|
let orb = SiriOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbSize, height: orbSize))
|
|
container.addSubview(orb)
|
|
self.orbView = orb
|
|
|
|
panel.contentView = container
|
|
self.window = panel
|
|
reposition()
|
|
}
|
|
|
|
private func reposition() {
|
|
guard let window = self.window, let screen = NSScreen.main else { return }
|
|
let screenRect = screen.visibleFrame
|
|
// Center horizontally, position near bottom of screen
|
|
let x = screenRect.midX - (canvasSize / 2.0)
|
|
let y = screenRect.minY + 12
|
|
window.setFrameOrigin(NSPoint(x: x, y: y))
|
|
}
|
|
|
|
public func show(state: HUDState) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
if self.window == nil {
|
|
self.setupWindow()
|
|
}
|
|
guard let window = self.window, let orb = self.orbView else { return }
|
|
self.reposition()
|
|
|
|
switch state {
|
|
case .hidden:
|
|
self.hide()
|
|
|
|
case .recording(let level, let isLagging):
|
|
if !self.isVisible {
|
|
window.orderFrontRegardless()
|
|
window.animator().alphaValue = 1.0
|
|
self.isVisible = true
|
|
}
|
|
orb.updateAudioLevel(level, isLagging: isLagging)
|
|
|
|
case .retrying:
|
|
if !self.isVisible {
|
|
window.orderFrontRegardless()
|
|
window.animator().alphaValue = 1.0
|
|
self.isVisible = true
|
|
}
|
|
orb.showRetryState()
|
|
|
|
case .transcribing:
|
|
orb.showTranscribingState()
|
|
|
|
case .success:
|
|
orb.showSuccessState()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) {
|
|
self.hide()
|
|
}
|
|
|
|
case .error:
|
|
orb.showErrorState()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
|
|
self.hide()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func hide() {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self, let window = self.window, self.isVisible else { return }
|
|
NSAnimationContext.runAnimationGroup({ context in
|
|
context.duration = 0.20
|
|
window.animator().alphaValue = 0.0
|
|
}, completionHandler: {
|
|
window.orderOut(nil)
|
|
self.isVisible = false
|
|
self.orbView?.reset()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Premium Apple Intelligence / Siri Multilayer Glowing Orb
|
|
|
|
final class SiriOrbView: NSView {
|
|
private let ambientAuraLayer = CALayer()
|
|
private let haloLayer = CALayer()
|
|
private let acousticRippleLayer = CALayer()
|
|
private let glassBackdrop = CALayer()
|
|
private let siriGradientLayer = CAGradientLayer()
|
|
private let specularHighlight = CAGradientLayer()
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
wantsLayer = true
|
|
setupLayers()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
wantsLayer = true
|
|
setupLayers()
|
|
}
|
|
|
|
private func setupLayers() {
|
|
guard let layer = self.layer else { return }
|
|
layer.masksToBounds = false
|
|
|
|
let bounds = self.bounds
|
|
let radius = bounds.width / 2.0
|
|
|
|
// 1. Deep Ambient Soft Aura (Diffused outer glow)
|
|
ambientAuraLayer.frame = bounds.insetBy(dx: -32, dy: -32)
|
|
ambientAuraLayer.cornerRadius = (bounds.width + 64) / 2.0
|
|
ambientAuraLayer.backgroundColor = NSColor(red: 0.0, green: 0.65, blue: 1.0, alpha: 0.22).cgColor
|
|
ambientAuraLayer.shadowColor = NSColor(red: 0.55, green: 0.0, blue: 1.0, alpha: 0.85).cgColor
|
|
ambientAuraLayer.shadowRadius = 38
|
|
ambientAuraLayer.shadowOpacity = 0.95
|
|
ambientAuraLayer.shadowOffset = .zero
|
|
layer.addSublayer(ambientAuraLayer)
|
|
|
|
// 2. Focused Neon Halo
|
|
haloLayer.frame = bounds.insetBy(dx: -14, dy: -14)
|
|
haloLayer.cornerRadius = (bounds.width + 28) / 2.0
|
|
haloLayer.backgroundColor = NSColor(red: 0.1, green: 0.8, blue: 1.0, alpha: 0.40).cgColor
|
|
haloLayer.shadowColor = NSColor(red: 0.0, green: 0.9, blue: 1.0, alpha: 0.85).cgColor
|
|
haloLayer.shadowRadius = 18
|
|
haloLayer.shadowOpacity = 0.9
|
|
haloLayer.shadowOffset = .zero
|
|
layer.addSublayer(haloLayer)
|
|
|
|
// 3. Acoustic Ripple Ring
|
|
acousticRippleLayer.frame = bounds.insetBy(dx: -4, dy: -4)
|
|
acousticRippleLayer.cornerRadius = (bounds.width + 8) / 2.0
|
|
acousticRippleLayer.borderColor = NSColor(red: 0.0, green: 0.95, blue: 1.0, alpha: 0.35).cgColor
|
|
acousticRippleLayer.borderWidth = 1.5
|
|
layer.addSublayer(acousticRippleLayer)
|
|
|
|
// 4. Dark Glass Spherical Backdrop
|
|
glassBackdrop.frame = bounds
|
|
glassBackdrop.cornerRadius = radius
|
|
glassBackdrop.backgroundColor = NSColor.black.withAlphaComponent(0.85).cgColor
|
|
glassBackdrop.borderColor = NSColor.white.withAlphaComponent(0.35).cgColor
|
|
glassBackdrop.borderWidth = 1.0
|
|
layer.addSublayer(glassBackdrop)
|
|
|
|
// 5. Multi-Color Liquid Siri Gradient Core
|
|
siriGradientLayer.frame = bounds.insetBy(dx: 3, dy: 3)
|
|
siriGradientLayer.cornerRadius = (bounds.width - 6) / 2.0
|
|
siriGradientLayer.type = .radial
|
|
siriGradientLayer.colors = [
|
|
NSColor(red: 0.0, green: 0.95, blue: 1.0, alpha: 1.0).cgColor, // Electric Cyan Center
|
|
NSColor(red: 0.50, green: 0.12, blue: 1.0, alpha: 0.95).cgColor, // Royal Violet
|
|
NSColor(red: 0.98, green: 0.08, blue: 0.65, alpha: 0.90).cgColor, // Hot Magenta
|
|
NSColor(red: 0.10, green: 0.20, blue: 0.95, alpha: 0.85).cgColor // Deep Blue
|
|
]
|
|
siriGradientLayer.startPoint = CGPoint(x: 0.35, y: 0.35)
|
|
siriGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
|
|
layer.addSublayer(siriGradientLayer)
|
|
|
|
// 6. 3D Curved Specular Glass Crescent Highlight (Apple Intelligence style)
|
|
specularHighlight.frame = NSRect(x: bounds.midX - 18, y: bounds.maxY - 18, width: 36, height: 12)
|
|
specularHighlight.cornerRadius = 6
|
|
specularHighlight.colors = [
|
|
NSColor.white.withAlphaComponent(0.65).cgColor,
|
|
NSColor.white.withAlphaComponent(0.0).cgColor
|
|
]
|
|
specularHighlight.startPoint = CGPoint(x: 0.5, y: 1.0)
|
|
specularHighlight.endPoint = CGPoint(x: 0.5, y: 0.0)
|
|
layer.addSublayer(specularHighlight)
|
|
|
|
// Start fluid rotation
|
|
startOrbRotation()
|
|
}
|
|
|
|
private func startOrbRotation() {
|
|
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
rotate.fromValue = 0.0
|
|
rotate.toValue = 2 * Double.pi
|
|
rotate.duration = 4.0
|
|
rotate.repeatCount = .infinity
|
|
rotate.isRemovedOnCompletion = false
|
|
siriGradientLayer.add(rotate, forKey: "siriFluidRotate")
|
|
}
|
|
|
|
func updateAudioLevel(_ level: Float, isLagging: Bool) {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
|
|
let clamped = CGFloat(min(max(level, 0.0), 1.0))
|
|
|
|
if isLagging {
|
|
// Warm amber pulse for weak internet
|
|
siriGradientLayer.colors = [
|
|
NSColor(red: 1.0, green: 0.85, blue: 0.2, alpha: 1.0).cgColor,
|
|
NSColor(red: 1.0, green: 0.55, blue: 0.0, alpha: 0.95).cgColor,
|
|
NSColor(red: 0.95, green: 0.25, blue: 0.0, alpha: 0.90).cgColor
|
|
]
|
|
haloLayer.backgroundColor = NSColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 0.5).cgColor
|
|
haloLayer.shadowColor = NSColor(red: 1.0, green: 0.4, blue: 0.0, alpha: 0.9).cgColor
|
|
ambientAuraLayer.shadowColor = NSColor(red: 1.0, green: 0.3, blue: 0.0, alpha: 0.7).cgColor
|
|
} else {
|
|
// High-fidelity Siri electric gradient
|
|
siriGradientLayer.colors = [
|
|
NSColor(red: 0.0, green: 0.95, blue: 1.0, alpha: 1.0).cgColor,
|
|
NSColor(red: 0.50, green: 0.12, blue: 1.0, alpha: 0.95).cgColor,
|
|
NSColor(red: 0.98, green: 0.08, blue: 0.65, alpha: 0.90).cgColor,
|
|
NSColor(red: 0.10, green: 0.20, blue: 0.95, alpha: 0.85).cgColor
|
|
]
|
|
haloLayer.backgroundColor = NSColor(red: 0.1, green: 0.8, blue: 1.0, alpha: 0.40).cgColor
|
|
haloLayer.shadowColor = NSColor(red: 0.0, green: 0.9, blue: 1.0, alpha: 0.85).cgColor
|
|
ambientAuraLayer.shadowColor = NSColor(red: 0.55, green: 0.0, blue: 1.0, alpha: 0.85).cgColor
|
|
}
|
|
|
|
// Massive Audio Dynamic Scaling (Orb expands up to 1.65x with voice)
|
|
let scale = 1.0 + (clamped * 0.65)
|
|
self.layer?.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
|
|
|
// Dynamic Halo bloom (Expands smoothly up to 55px radius)
|
|
haloLayer.shadowRadius = 18 + (clamped * 36)
|
|
haloLayer.shadowOpacity = Float(0.75 + (clamped * 0.25))
|
|
ambientAuraLayer.shadowRadius = 38 + (clamped * 42)
|
|
|
|
// Acoustic ripple expands outward
|
|
let rippleScale = 1.0 + (clamped * 0.35)
|
|
acousticRippleLayer.transform = CATransform3DMakeScale(rippleScale, rippleScale, 1.0)
|
|
acousticRippleLayer.opacity = Float(0.3 + (clamped * 0.7))
|
|
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func showRetryState() {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(false)
|
|
CATransaction.setAnimationDuration(0.3)
|
|
siriGradientLayer.colors = [
|
|
NSColor(red: 1.0, green: 0.85, blue: 0.2, alpha: 1.0).cgColor,
|
|
NSColor(red: 1.0, green: 0.50, blue: 0.0, alpha: 0.95).cgColor,
|
|
NSColor(red: 0.95, green: 0.20, blue: 0.0, alpha: 0.90).cgColor
|
|
]
|
|
haloLayer.shadowColor = NSColor.orange.cgColor
|
|
ambientAuraLayer.shadowColor = NSColor.orange.cgColor
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func showTranscribingState() {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
self.layer?.sublayerTransform = CATransform3DMakeScale(1.08, 1.08, 1.0)
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func showSuccessState() {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(false)
|
|
CATransaction.setAnimationDuration(0.2)
|
|
siriGradientLayer.colors = [
|
|
NSColor(red: 0.2, green: 1.0, blue: 0.5, alpha: 1.0).cgColor,
|
|
NSColor(red: 0.0, green: 0.85, blue: 0.8, alpha: 0.95).cgColor
|
|
]
|
|
haloLayer.shadowColor = NSColor.green.cgColor
|
|
ambientAuraLayer.shadowColor = NSColor.green.cgColor
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func showErrorState() {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(false)
|
|
CATransaction.setAnimationDuration(0.2)
|
|
siriGradientLayer.colors = [
|
|
NSColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 1.0).cgColor,
|
|
NSColor(red: 0.8, green: 0.0, blue: 0.4, alpha: 0.95).cgColor
|
|
]
|
|
haloLayer.shadowColor = NSColor.red.cgColor
|
|
ambientAuraLayer.shadowColor = NSColor.red.cgColor
|
|
CATransaction.commit()
|
|
}
|
|
|
|
func reset() {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
self.layer?.sublayerTransform = CATransform3DIdentity
|
|
haloLayer.shadowRadius = 18
|
|
ambientAuraLayer.shadowRadius = 38
|
|
acousticRippleLayer.transform = CATransform3DIdentity
|
|
acousticRippleLayer.opacity = 0.3
|
|
CATransaction.commit()
|
|
}
|
|
}
|