From 01688806b42cf332c21d0b9c276ee50d08e9f804 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Fri, 25 Sep 2026 20:13:09 +0000 Subject: [PATCH] feat: purely outward expanding dynamic stroke (fixed solid black core), neon crimson red voice pulse with emerald green commit flash --- src/HUDOverlay.swift | 150 +++++++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 64 deletions(-) diff --git a/src/HUDOverlay.swift b/src/HUDOverlay.swift index f35cf91..2a4a619 100644 --- a/src/HUDOverlay.swift +++ b/src/HUDOverlay.swift @@ -14,10 +14,10 @@ public final class HUDOverlayController { public static let shared = HUDOverlayController() private var window: NSPanel? - private var orbView: MinimalSolidOrbView? + private var orbView: OutwardExpandingOrbView? private var isVisible = false - // Transparent canvas with plenty of margin + // Large transparent canvas so outward expansion and shadows never clip private let canvasSize: CGFloat = 200 private init() { @@ -44,10 +44,7 @@ public final class HUDOverlayController { let container = NSView(frame: panel.contentView!.bounds) container.wantsLayer = true - // Solid Minimal Orb centered in canvas - let orbDiameter: CGFloat = 52 - let orbOrigin = (canvasSize - orbDiameter) / 2.0 - let orb = MinimalSolidOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbDiameter, height: orbDiameter)) + let orb = OutwardExpandingOrbView(frame: NSRect(x: 0, y: 0, width: canvasSize, height: canvasSize)) container.addSubview(orb) self.orbView = orb @@ -99,7 +96,7 @@ public final class HUDOverlayController { case .success: orb.showSuccessState() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.40) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { self.hide() } @@ -127,15 +124,16 @@ public final class HUDOverlayController { } } -// MARK: - Minimal Solid-Color Orb with Single High-Amplitude Dynamic Stroke (Zero Static Rings) +// MARK: - Outward Expanding Solid Orb View +// Core remains completely fixed in size; Dynamic stroke expands 100% OUTWARD into the surrounding space. -final class MinimalSolidOrbView: NSView { +final class OutwardExpandingOrbView: NSView { private let coreSolidLayer = CALayer() private let dynamicStrokeLayer = CALayer() - // Wide dynamic range: from a delicate 1.5px ring up to a bold 11.5px pulse! - private let minStrokeWidth: CGFloat = 1.5 - private let maxStrokeWidth: CGFloat = 11.5 + private let baseDiameter: CGFloat = 48.0 + private let minStrokeWidth: CGFloat = 2.0 + private let maxStrokeWidth: CGFloat = 12.0 override init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -153,65 +151,68 @@ final class MinimalSolidOrbView: NSView { guard let layer = self.layer else { return } layer.masksToBounds = false - let bounds = self.bounds - let radius = bounds.width / 2.0 + let center = CGPoint(x: bounds.midX, y: bounds.midY) - // 1. Chic, Minimal Solid-Color Core (Apple Obsidian Charcoal Solid Disc) - // 100% solid, perfectly flat, clean and matte - coreSolidLayer.frame = bounds - coreSolidLayer.cornerRadius = radius - coreSolidLayer.backgroundColor = NSColor(red: 0.09, green: 0.10, blue: 0.13, alpha: 0.98).cgColor + // 1. Solid Jet Black Inner Core (Fixed 48px circle, NEVER resizes or shrinks) + coreSolidLayer.bounds = CGRect(x: 0, y: 0, width: baseDiameter, height: baseDiameter) + coreSolidLayer.position = center + coreSolidLayer.cornerRadius = baseDiameter / 2.0 + coreSolidLayer.backgroundColor = NSColor(red: 0.08, green: 0.08, blue: 0.10, alpha: 1.0).cgColor layer.addSublayer(coreSolidLayer) - // 2. Single Dynamic Audio-Reactive Stroke Ring (NO SECOND RING / NO STATIC BACKGROUND) - // Solid Electric Amethyst Violet (#B066FF) - dynamicStrokeLayer.frame = bounds - dynamicStrokeLayer.cornerRadius = radius + // 2. Outward Expanding Dynamic Crimson Red Stroke Layer + let initialStrokeDiameter = baseDiameter + (minStrokeWidth * 2.0) + dynamicStrokeLayer.bounds = CGRect(x: 0, y: 0, width: initialStrokeDiameter, height: initialStrokeDiameter) + dynamicStrokeLayer.position = center + dynamicStrokeLayer.cornerRadius = initialStrokeDiameter / 2.0 dynamicStrokeLayer.borderWidth = minStrokeWidth - dynamicStrokeLayer.borderColor = NSColor(red: 0.70, green: 0.38, blue: 1.0, alpha: 0.75).cgColor + dynamicStrokeLayer.borderColor = NSColor(red: 1.0, green: 0.18, blue: 0.33, alpha: 0.85).cgColor // Vibrant Neon Red dynamicStrokeLayer.backgroundColor = NSColor.clear.cgColor - dynamicStrokeLayer.shadowColor = NSColor(red: 0.65, green: 0.25, blue: 1.0, alpha: 0.85).cgColor + dynamicStrokeLayer.shadowColor = NSColor(red: 1.0, green: 0.10, blue: 0.30, alpha: 0.90).cgColor dynamicStrokeLayer.shadowOffset = .zero dynamicStrokeLayer.shadowRadius = 4.0 - dynamicStrokeLayer.shadowOpacity = 0.40 + dynamicStrokeLayer.shadowOpacity = 0.50 layer.addSublayer(dynamicStrokeLayer) } - /// Updates audio response with boosted logarithmic curve for wide dynamic amplitude: - /// In quiet: minimal 1.5px stroke. - /// When speaking: expands dramatically up to 11.5px with bright electric violet glow. + /// Updates audio response: + /// The inner black core stays completely fixed. + /// The stroke width expands purely OUTWARD from 2.0px up to 12.0px with voice volume. func updateAudioLevel(_ level: Float, isLagging: Bool) { CATransaction.begin() CATransaction.setDisableActions(true) let clamped = CGFloat(min(max(level, 0.0), 1.0)) - // Perceptual boost power curve so lower/normal voice levels trigger a bold visible response let dynamicFactor = pow(clamped, 0.70) + let strokeWidth = minStrokeWidth + (dynamicFactor * (maxStrokeWidth - minStrokeWidth)) + let strokeDiameter = baseDiameter + (strokeWidth * 2.0) + + // Outward expansion geometry: inner edge = strokeDiameter - 2*strokeWidth = baseDiameter (48.0) + dynamicStrokeLayer.bounds = CGRect(x: 0, y: 0, width: strokeDiameter, height: strokeDiameter) + dynamicStrokeLayer.cornerRadius = strokeDiameter / 2.0 + dynamicStrokeLayer.borderWidth = strokeWidth + if isLagging { - // Warm amber gold highlight for degraded network - let amber = NSColor(red: 1.0, green: 0.68, blue: 0.15, alpha: 0.75 + (0.25 * dynamicFactor)).cgColor + // Warm amber gold indicator for degraded network + let amber = NSColor(red: 1.0, green: 0.68, blue: 0.15, alpha: 0.80 + (0.20 * dynamicFactor)).cgColor dynamicStrokeLayer.borderColor = amber dynamicStrokeLayer.shadowColor = amber } else { - // Solid Electric Amethyst Violet palette: brightens to glowing neon with volume - let violetStroke = NSColor( - red: 0.70 + (0.20 * dynamicFactor), - green: 0.38 + (0.50 * dynamicFactor), - blue: 1.0, - alpha: 0.75 + (0.25 * dynamicFactor) + // Fiery Neon Crimson Red palette: brightens with volume + let neonRed = NSColor( + red: 1.0, + green: 0.18 + (0.35 * dynamicFactor), + blue: 0.33 * (1.0 - dynamicFactor), + alpha: 0.80 + (0.20 * dynamicFactor) ).cgColor - dynamicStrokeLayer.borderColor = violetStroke - dynamicStrokeLayer.shadowColor = NSColor(red: 0.65, green: 0.25, blue: 1.0, alpha: 0.85).cgColor + dynamicStrokeLayer.borderColor = neonRed + dynamicStrokeLayer.shadowColor = NSColor(red: 1.0, green: 0.15, blue: 0.25, alpha: 0.90).cgColor } - // 1. High Dynamic Amplitude Stroke Expansion (1.5px to 11.5px) - let dynamicWidth = minStrokeWidth + (dynamicFactor * (maxStrokeWidth - minStrokeWidth)) - dynamicStrokeLayer.borderWidth = dynamicWidth - - // 2. Soft Dynamic Bloom Glow (From 4px to 26px radius, strictly tracking voice) - dynamicStrokeLayer.shadowRadius = 4.0 + (dynamicFactor * 22.0) - dynamicStrokeLayer.shadowOpacity = Float(0.40 + (0.55 * dynamicFactor)) + // Dynamic bloom shadow expanding outward with voice + dynamicStrokeLayer.shadowRadius = 4.0 + (dynamicFactor * 24.0) + dynamicStrokeLayer.shadowOpacity = Float(0.50 + (0.45 * dynamicFactor)) CATransaction.commit() } @@ -221,7 +222,11 @@ final class MinimalSolidOrbView: NSView { CATransaction.setDisableActions(false) CATransaction.setAnimationDuration(0.20) let amber = NSColor(red: 1.0, green: 0.65, blue: 0.10, alpha: 1.0).cgColor - dynamicStrokeLayer.borderWidth = 5.0 + let strokeWidth: CGFloat = 5.0 + let strokeDiameter = baseDiameter + (strokeWidth * 2.0) + dynamicStrokeLayer.bounds = CGRect(x: 0, y: 0, width: strokeDiameter, height: strokeDiameter) + dynamicStrokeLayer.cornerRadius = strokeDiameter / 2.0 + dynamicStrokeLayer.borderWidth = strokeWidth dynamicStrokeLayer.borderColor = amber dynamicStrokeLayer.shadowColor = amber dynamicStrokeLayer.shadowRadius = 18 @@ -232,23 +237,32 @@ final class MinimalSolidOrbView: NSView { func showTranscribingState() { CATransaction.begin() CATransaction.setDisableActions(true) - let softLilac = NSColor(red: 0.80, green: 0.50, blue: 1.0, alpha: 1.0).cgColor - dynamicStrokeLayer.borderWidth = 3.5 - dynamicStrokeLayer.borderColor = softLilac - dynamicStrokeLayer.shadowRadius = 10 - dynamicStrokeLayer.shadowOpacity = 0.65 + let strokeWidth: CGFloat = 3.5 + let strokeDiameter = baseDiameter + (strokeWidth * 2.0) + dynamicStrokeLayer.bounds = CGRect(x: 0, y: 0, width: strokeDiameter, height: strokeDiameter) + dynamicStrokeLayer.cornerRadius = strokeDiameter / 2.0 + dynamicStrokeLayer.borderWidth = strokeWidth + dynamicStrokeLayer.borderColor = NSColor.white.cgColor + dynamicStrokeLayer.shadowColor = NSColor.white.cgColor + dynamicStrokeLayer.shadowRadius = 12 + dynamicStrokeLayer.shadowOpacity = 0.70 CATransaction.commit() } + /// User requested: When text is committed/inserted, the stroke turns vibrant Emerald Green! func showSuccessState() { CATransaction.begin() CATransaction.setDisableActions(false) CATransaction.setAnimationDuration(0.18) - let green = NSColor(red: 0.15, green: 0.95, blue: 0.45, alpha: 1.0).cgColor - dynamicStrokeLayer.borderWidth = 5.0 - dynamicStrokeLayer.borderColor = green - dynamicStrokeLayer.shadowColor = green - dynamicStrokeLayer.shadowRadius = 20 + let emerald = NSColor(red: 0.18, green: 0.85, blue: 0.38, alpha: 1.0).cgColor // Vibrant Emerald Green + let strokeWidth: CGFloat = 6.0 + let strokeDiameter = baseDiameter + (strokeWidth * 2.0) + dynamicStrokeLayer.bounds = CGRect(x: 0, y: 0, width: strokeDiameter, height: strokeDiameter) + dynamicStrokeLayer.cornerRadius = strokeDiameter / 2.0 + dynamicStrokeLayer.borderWidth = strokeWidth + dynamicStrokeLayer.borderColor = emerald + dynamicStrokeLayer.shadowColor = emerald + dynamicStrokeLayer.shadowRadius = 24 dynamicStrokeLayer.shadowOpacity = 0.95 CATransaction.commit() } @@ -257,10 +271,14 @@ final class MinimalSolidOrbView: NSView { CATransaction.begin() CATransaction.setDisableActions(false) CATransaction.setAnimationDuration(0.18) - let red = NSColor(red: 1.0, green: 0.20, blue: 0.20, alpha: 1.0).cgColor - dynamicStrokeLayer.borderWidth = 5.0 - dynamicStrokeLayer.borderColor = red - dynamicStrokeLayer.shadowColor = red + let darkRed = NSColor(red: 0.95, green: 0.15, blue: 0.15, alpha: 1.0).cgColor + let strokeWidth: CGFloat = 5.0 + let strokeDiameter = baseDiameter + (strokeWidth * 2.0) + dynamicStrokeLayer.bounds = CGRect(x: 0, y: 0, width: strokeDiameter, height: strokeDiameter) + dynamicStrokeLayer.cornerRadius = strokeDiameter / 2.0 + dynamicStrokeLayer.borderWidth = strokeWidth + dynamicStrokeLayer.borderColor = darkRed + dynamicStrokeLayer.shadowColor = darkRed dynamicStrokeLayer.shadowRadius = 20 dynamicStrokeLayer.shadowOpacity = 0.95 CATransaction.commit() @@ -269,10 +287,14 @@ final class MinimalSolidOrbView: NSView { func reset() { CATransaction.begin() CATransaction.setDisableActions(true) + let initialStrokeDiameter = baseDiameter + (minStrokeWidth * 2.0) + dynamicStrokeLayer.bounds = CGRect(x: 0, y: 0, width: initialStrokeDiameter, height: initialStrokeDiameter) + dynamicStrokeLayer.cornerRadius = initialStrokeDiameter / 2.0 dynamicStrokeLayer.borderWidth = minStrokeWidth - dynamicStrokeLayer.borderColor = NSColor(red: 0.70, green: 0.38, blue: 1.0, alpha: 0.75).cgColor + dynamicStrokeLayer.borderColor = NSColor(red: 1.0, green: 0.18, blue: 0.33, alpha: 0.85).cgColor + dynamicStrokeLayer.shadowColor = NSColor(red: 1.0, green: 0.10, blue: 0.30, alpha: 0.90).cgColor dynamicStrokeLayer.shadowRadius = 4.0 - dynamicStrokeLayer.shadowOpacity = 0.40 + dynamicStrokeLayer.shadowOpacity = 0.50 CATransaction.commit() } }