Browse Source

feat: purely outward expanding dynamic stroke (fixed solid black core), neon crimson red voice pulse with emerald green commit flash

main
Ali Alavi 2 weeks ago
parent
commit
01688806b4
  1. 150
      src/HUDOverlay.swift

150
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()
}
}
Loading…
Cancel
Save