From 74f3a02ebb2d0062fec52c13369363584875500e Mon Sep 17 00:00:00 2001 From: aichristabasco Date: Sat, 21 Feb 2026 14:38:37 +0000 Subject: [PATCH] feat: premium overlay redesign - fade animation, glass panel, glow border - Smooth fade-in / fade-out animation (alpha 0->0.95 and back) - Layered purple glow shadow behind the panel for depth - Glass panel with top-edge shine highlight - Purple glow border (no harsh solid rectangle) - Transparent background using key-colour (#010101) - 4-colour gradient waveform bars: dark-purple -> violet -> rose - Larger pulsing glow rings around the record dot - Overlay size: 520x68px, bottom-center of screen --- voxflow/overlay.py | 288 ++++++++++++++++++++++----------------------- 1 file changed, 144 insertions(+), 144 deletions(-) diff --git a/voxflow/overlay.py b/voxflow/overlay.py index 179f7e7..26d0d10 100644 --- a/voxflow/overlay.py +++ b/voxflow/overlay.py @@ -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 @@ -13,19 +10,11 @@ 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 @@ -33,71 +22,55 @@ def __init__(self): 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.0–1.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}") @@ -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)