From 88327017a5a5f6193a3416abdc7926bcd48403f0 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Fri, 25 Sep 2026 20:03:10 +0000 Subject: [PATCH] feat: solid-color graphite orb, 100% audio-driven stroke expansion (up to 8px) and bloom halo vanishing in silence, higher vertical placement --- src/HUDOverlay.swift | 188 ++++++++++++++++++++++--------------------- 1 file changed, 98 insertions(+), 90 deletions(-) diff --git a/src/HUDOverlay.swift b/src/HUDOverlay.swift index 3aac2b6..e40a7d0 100644 --- a/src/HUDOverlay.swift +++ b/src/HUDOverlay.swift @@ -14,11 +14,11 @@ public final class HUDOverlayController { public static let shared = HUDOverlayController() private var window: NSPanel? - private var orbView: MinimalSiriOrbView? + private var orbView: SolidSiriOrbView? 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() { DispatchQueue.main.async { @@ -44,10 +44,10 @@ public final class HUDOverlayController { let container = NSView(frame: panel.contentView!.bounds) 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 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) self.orbView = orb @@ -59,8 +59,9 @@ public final class HUDOverlayController { private func reposition() { guard let window = self.window, let screen = NSScreen.main else { return } let screenRect = screen.visibleFrame + // Center horizontally, positioned comfortably higher above the dock let x = screenRect.midX - (canvasSize / 2.0) - let y = screenRect.minY + 28 + let y = screenRect.minY + 75 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 gradientCoreLayer = CAGradientLayer() + private let solidCoreLayer = 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) { super.init(frame: frameRect) @@ -156,103 +155,103 @@ final class MinimalSiriOrbView: NSView { let bounds = self.bounds 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.opacity = 0.0 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) + // 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 Audio-Reactive Stroke Ring (Widening & brightening with speech) + // 3. Dynamic Voice-Reactive Stroke Ring (Disappears completely in silence) strokeBorderLayer.frame = bounds 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.opacity = 0.0 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) { CATransaction.begin() 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 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 + 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 { - // 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. 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 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) } - // 1. Dynamic Stroke Width: from 1.5px up to 5.5px based on voice volume - let dynamicWidth = minStrokeWidth + (clamped * (maxStrokeWidth - minStrokeWidth)) - strokeBorderLayer.borderWidth = dynamicWidth - - // 2. Dynamic Outer Glow: blooms softly with speech - glowLayer.shadowRadius = 14 + (clamped * 24) - glowLayer.shadowOpacity = Float(0.60 + (clamped * 0.35)) - CATransaction.commit() } func showRetryState() { CATransaction.begin() 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 glowLayer.shadowColor = NSColor.orange.cgColor - glowLayer.shadowRadius = 22 + glowLayer.shadowRadius = 24 + glowLayer.shadowOpacity = 0.9 + glowLayer.opacity = 1.0 CATransaction.commit() } func showTranscribingState() { CATransaction.begin() 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() } @@ -260,10 +259,14 @@ final class MinimalSiriOrbView: NSView { CATransaction.begin() CATransaction.setDisableActions(false) 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() } @@ -271,20 +274,25 @@ final class MinimalSiriOrbView: NSView { CATransaction.begin() CATransaction.setDisableActions(false) 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() } func reset() { CATransaction.begin() 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() } }