Browse Source

feat: solid-color graphite orb, 100% audio-driven stroke expansion (up to 8px) and bloom halo vanishing in silence, higher vertical placement

main
Ali Alavi 2 weeks ago
parent
commit
88327017a5
  1. 190
      src/HUDOverlay.swift

190
src/HUDOverlay.swift

@ -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))
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
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 { } 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
}
// Active speech: scale dynamic intensity noticeably
let norm = (rawClamped - silenceThreshold) / (1.0 - silenceThreshold)
if isLagging {
// 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 {
// 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))
strokeBorderLayer.borderWidth = dynamicWidth
// 1. Noticeably powerful stroke expansion (from 0 to 8px)
let dynamicWidth = norm * maxStrokeWidth
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()
} }
} }
Loading…
Cancel
Save