|
|
|
@ -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() |
|
|
|
} |
|
|
|
} |