Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 144 additions & 144 deletions voxflow/overlay.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
"""VoxFlow Recording Overlay — On-screen animation while recording.

Shows a sleek floating bar at the BOTTOM-CENTER of the screen with
an animated audio waveform. The overlay is transparent and click-through
so it doesn't interfere with the user's work.
"""VoxFlow Recording Overlay - Bottom-center animated waveform bar.

Shows only while recording (show/hide called from app.py).
Built by AI Evolution Polska
"""
import threading
Expand All @@ -13,91 +10,67 @@


class RecordingOverlay:
"""Floating overlay that shows recording waveform animation at bottom of screen."""

WIDTH = 560
HEIGHT = 72
BOTTOM_MARGIN = 80 # pixels from bottom of screen
"""Floating overlay that shows recording waveform at bottom-center of screen."""

# Colours
BG = "#0d0d1f"
BORDER = "#7c3aed"
RED = "#ef4444"
PURPLE = "#a78bfa"
PURPLE2 = "#7c3aed"
GREY = "#94a3b8"
W = 520
H = 68
MARGIN_BOTTOM = 70

def __init__(self):
self._root: Optional[tk.Toplevel] = None
self._canvas: Optional[tk.Canvas] = None
self._alive = False
self._phase = 0.0
self._level = 0.0
self._alpha = 0.0 # current opacity (for fade in/out)
self._fading_out = False
self._parent = None

# ── Public API ────────────────────────────────────────────────

def show(self, parent=None):
"""Show the recording overlay on screen."""
"""Show the overlay — called when recording starts."""
if self._alive:
return
self._parent = parent
self._alive = True
self._fading_out = False
self._alpha = 0.0
if parent:
parent.after(0, self._create_window)
else:
threading.Thread(target=self._run_standalone, daemon=True).start()

def hide(self):
"""Hide the recording overlay."""
"""Hide the overlay — called when recording stops."""
self._alive = False
if self._root and self._parent:
try:
self._parent.after(0, self._destroy_window)
except Exception:
pass
elif self._root:
try:
self._root.destroy()
except Exception:
pass
self._root = None
self._canvas = None
self._fading_out = True

def set_level(self, level: float):
"""Update the audio level (0.0 - 1.0)."""
"""Update audio level (0.01.0) for waveform animation."""
self._level = min(1.0, max(0.0, level))

# ── Window management ─────────────────────────────────────────
# ── Window ────────────────────────────────────────────────────

def _create_window(self):
"""Create the overlay window (must be called from main thread)."""
try:
if self._parent:
self._root = tk.Toplevel(self._parent)
else:
self._root = tk.Tk()

self._root = tk.Toplevel(self._parent)
win = self._root
win.overrideredirect(True) # No title bar / borders
win.attributes("-topmost", True) # Always on top
win.attributes("-alpha", 0.93) # Slightly transparent

# Position: bottom-center of screen
screen_w = win.winfo_screenwidth()
screen_h = win.winfo_screenheight()
x = (screen_w - self.WIDTH) // 2
y = screen_h - self.HEIGHT - self.BOTTOM_MARGIN
win.geometry(f"{self.WIDTH}x{self.HEIGHT}+{x}+{y}")

win.configure(bg=self.BG)
win.overrideredirect(True)
win.attributes("-topmost", True)
win.attributes("-alpha", 0.0) # start invisible, fade in
win.attributes("-transparentcolor", "#010101") # key-colour transparency

sw = win.winfo_screenwidth()
sh = win.winfo_screenheight()
x = (sw - self.W) // 2
y = sh - self.H - self.MARGIN_BOTTOM
win.geometry(f"{self.W}x{self.H}+{x}+{y}")
win.configure(bg="#010101")

self._canvas = tk.Canvas(
win, width=self.WIDTH, height=self.HEIGHT,
bg=self.BG, highlightthickness=0,
win, width=self.W, height=self.H,
bg="#010101", highlightthickness=0,
)
self._canvas.pack(fill="both", expand=True)

self._canvas.pack()
self._animate()
except Exception as e:
print(f"Overlay error: {e}")
Expand All @@ -110,112 +83,139 @@ def _destroy_window(self):
pass
self._root = None
self._canvas = None
self._fading_out = False

def _run_standalone(self):
self._create_window()
if self._root:
self._root.mainloop()

# ── Animation ─────────────────────────────────────────────────
# ── Animation loop ────────────────────────────────────────────

def _animate(self):
if not self._alive or not self._canvas:
if not self._canvas or not self._root:
return

self._phase += 0.12
# --- Fade in / fade out ---
if self._fading_out:
self._alpha = max(0.0, self._alpha - 0.07)
try:
self._root.attributes("-alpha", self._alpha)
except Exception:
pass
if self._alpha <= 0.0:
if self._parent:
self._parent.after(0, self._destroy_window)
return
elif self._alpha < 0.95:
self._alpha = min(0.95, self._alpha + 0.06)
try:
self._root.attributes("-alpha", self._alpha)
except Exception:
pass

self._phase += 0.11
self._draw()

try:
self._root.after(35, self._animate)
except Exception:
pass

def _draw(self):
c = self._canvas
W, H = self.WIDTH, self.HEIGHT
W, H = self.W, self.H
c.delete("all")

# ── Rounded background panel ──────────────────────────────
self._round_rect(c, 2, 2, W - 2, H - 2, r=16,
fill=self.BG, outline=self.BORDER, width=2)

# ── Pulsing mic icon (left side) ──────────────────────────
pulse = (math.sin(self._phase * 2.5) + 1) / 2
cx, cy = 36, H // 2

# Outer glow ring
glow_r = 18 + pulse * 5
c.create_oval(cx - glow_r, cy - glow_r,
cx + glow_r, cy + glow_r,
fill="", outline=self.RED, width=1)

# Inner filled dot
dot_r = 12 + pulse * 3
c.create_oval(cx - dot_r, cy - dot_r,
cx + dot_r, cy + dot_r,
fill=self.RED, outline="")

# Mic body (white)
c.create_oval(cx - 5, cy - 10, cx + 5, cy + 1,
fill="white", outline="")
# Mic stand
c.create_line(cx, cy + 1, cx, cy + 8, fill="white", width=2)
c.create_line(cx - 6, cy + 8, cx + 6, cy + 8, fill="white", width=2)

# ── Label text ────────────────────────────────────────────
c.create_text(64, cy - 10, text="🎤 Nagrywam...",
fill=self.RED, font=("Segoe UI", 11, "bold"), anchor="w")
c.create_text(64, cy + 9, text="VoxFlow • zwolnij klawisz aby zakończyć",
fill=self.GREY, font=("Segoe UI", 8), anchor="w")
# ── Outer glow (shadow layer) ─────────────────────────────
# Subtle purple glow border drawn as stacked rounded rects
for i in range(4, 0, -1):
alpha_colors = ["#1a0a2e", "#1e0d38", "#220f42", "#26114a"]
self._rrect(c, i, i, W - i, H - i, r=18, fill=alpha_colors[i-1], outline="")

# ── Glass panel ───────────────────────────────────────────
self._rrect(c, 3, 3, W - 3, H - 3, r=16,
fill="#0d0b1e", outline="#6d28d9", width=2)

# Inner highlight line (top edge shine)
self._rrect(c, 4, 4, W - 4, 18, r=14,
fill="#ffffff08", outline="")

# ── Pulsing record dot (left) ─────────────────────────────
pulse = (math.sin(self._phase * 3.0) + 1) / 2
cx, cy = 38, H // 2

# Outer glow rings
for ri, col, fade in [(22, "#ef444420", 1.0),
(17, "#ef444440", 1.0),
(13, "#ef444480", 1.0)]:
r = ri + pulse * 3
c.create_oval(cx - r, cy - r, cx + r, cy + r,
fill=col, outline="")

# Core dot
dr = 9 + pulse * 2
c.create_oval(cx - dr, cy - dr, cx + dr, cy + dr,
fill="#ef4444", outline="#fca5a5", width=1)

# Tiny white mic icon inside dot
c.create_oval(cx - 3, cy - 6, cx + 3, cy, fill="white", outline="")
c.create_line(cx, cy, cx, cy + 5, fill="white", width=2)
c.create_line(cx - 4, cy + 5, cx + 4, cy + 5, fill="white", width=2)

# ── Text labels ───────────────────────────────────────────
c.create_text(62, cy - 9,
text="Nagrywam...",
fill="#fca5a5", font=("Segoe UI", 11, "bold"),
anchor="w")
c.create_text(62, cy + 8,
text="VoxFlow \u2022 zwolnij klawisz aby zakonczyc",
fill="#7c6fa0", font=("Segoe UI", 8),
anchor="w")

# ── Waveform bars (right section) ─────────────────────────
num_bars = 28
bar_w = 4
bar_gap = 2
bar_section_x = 240
max_h = 22

for i in range(num_bars):
# Combine audio level with animated sine waves
t = self._phase * 2.8 + i * 0.42
wave1 = abs(math.sin(t))
wave2 = abs(math.sin(t * 1.37 + 1.2)) * 0.5
combined = wave1 * 0.55 + wave2 * 0.15

# Boost by real audio level
amp = (combined + self._level * 0.6) * max_h + 3
n_bars = 30
bw = 4
gap = 2
x0 = 230
max_h = 20

for i in range(n_bars):
t = self._phase * 2.6 + i * 0.38
wave = abs(math.sin(t)) * 0.6 + abs(math.sin(t * 0.7 + 1.1)) * 0.4
amp = (wave * 0.5 + self._level * 0.65) * max_h + 3
amp = min(amp, max_h)

x = bar_section_x + i * (bar_w + bar_gap)
x = x0 + i * (bw + gap)
bar_cy = cy

# Colour gradient: centre bars brighter
# Gradient colour: low=purple, mid=violet, high=rose
ratio = amp / max_h
if ratio > 0.75:
color = self.RED
elif ratio > 0.45:
color = self.PURPLE
if ratio > 0.78:
col = "#f43f5e"
elif ratio > 0.50:
col = "#a78bfa"
elif ratio > 0.28:
col = "#7c3aed"
else:
color = self.PURPLE2
col = "#4c1d95"

# Draw symmetric bar
c.create_rectangle(
x, bar_cy - amp, x + bar_w, bar_cy + amp,
fill=color, outline="",
)
# Rounded cap top
c.create_oval(
x, bar_cy - amp - 2, x + bar_w, bar_cy - amp + 2,
fill=color, outline="",
)
# Bar body
c.create_rectangle(x, bar_cy - amp, x + bw, bar_cy + amp,
fill=col, outline="")
# Rounded cap
c.create_oval(x - 1, bar_cy - amp - 2, x + bw + 1, bar_cy - amp + 2,
fill=col, outline="")

try:
if self._root:
self._root.after(40, self._animate)
except Exception:
pass
# ── Right accent line ─────────────────────────────────────
rx = x0 + n_bars * (bw + gap) + 6
c.create_line(rx, 12, rx, H - 12,
fill="#6d28d950", width=1)

# ── Helper ────────────────────────────────────────────────────

@staticmethod
def _round_rect(canvas, x1, y1, x2, y2, r=12, **kw):
"""Draw a rounded rectangle on a tk Canvas."""
canvas.create_arc(x1, y1, x1+2*r, y1+2*r, start=90, extent=90, style="pieslice", **kw)
canvas.create_arc(x2-2*r, y1, x2, y1+2*r, start=0, extent=90, style="pieslice", **kw)
canvas.create_arc(x1, y2-2*r, x1+2*r, y2, start=180, extent=90, style="pieslice", **kw)
canvas.create_arc(x2-2*r, y2-2*r, x2, y2, start=270, extent=90, style="pieslice", **kw)
canvas.create_rectangle(x1+r, y1, x2-r, y2, **kw)
canvas.create_rectangle(x1, y1+r, x1+r, y2-r, **kw)
canvas.create_rectangle(x2-r, y1+r, x2, y2-r, **kw)
def _rrect(canvas, x1, y1, x2, y2, r=12, **kw):
"""Draw a rounded rectangle."""
canvas.create_arc(x1, y1, x1+2*r, y1+2*r, start=90, extent=90, style="pieslice", **kw)
canvas.create_arc(x2 - 2*r, y1, x2, y1+2*r, start=0, extent=90, style="pieslice", **kw)
canvas.create_arc(x1, y2 - 2*r, x1+2*r, y2, start=180, extent=90, style="pieslice", **kw)
canvas.create_arc(x2 - 2*r, y2 - 2*r, x2, y2, start=270, extent=90, style="pieslice", **kw)
canvas.create_rectangle(x1 + r, y1, x2 - r, y2, **kw)
canvas.create_rectangle(x1, y1 + r, x1 + r, y2 - r, **kw)
canvas.create_rectangle(x2 - r, y1 + r, x2, y2 - r, **kw)