Commit ·
4a5df81
1
Parent(s): 70d2074
Enhance user experience by clearing 'Waiting for approval' status immediately upon approval and refining markdown table styles for better readability and aesthetics.
Browse files- agent/utils/boot_timing.py +15 -0
- agent/utils/crt_boot.py +113 -0
- agent/utils/particle_logo.py +228 -0
agent/utils/boot_timing.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared timing and color helpers for startup visual effects."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def settle_curve(progress: float, sharpness: float = 3.0) -> float:
|
| 7 |
+
"""Return noise amount in range 1..0 for normalized progress 0..1."""
|
| 8 |
+
t = max(0.0, min(1.0, progress))
|
| 9 |
+
return math.exp(-sharpness * t)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def warm_gold_from_white(progress: float) -> tuple[int, int, int]:
|
| 13 |
+
"""Interpolate from white to warm gold for progress 0..1."""
|
| 14 |
+
t = max(0.0, min(1.0, progress))
|
| 15 |
+
return 255, int(255 - 55 * t), int(255 - 175 * t)
|
agent/utils/crt_boot.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CRT / glitch boot sequence effect for CLI startup.
|
| 2 |
+
|
| 3 |
+
Simulates an old CRT terminal booting up: text appearing character by character
|
| 4 |
+
with noise artifacts, then settling into a clean display.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import random
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
from rich.console import Console
|
| 11 |
+
from rich.text import Text
|
| 12 |
+
from rich.live import Live
|
| 13 |
+
|
| 14 |
+
from agent.utils.boot_timing import settle_curve
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _glitch_text(text: str, intensity: float, rng: random.Random) -> str:
|
| 18 |
+
"""Add random glitch characters to text."""
|
| 19 |
+
glitch_chars = "█▓▒░┃┫┣╋╏╎─━┅┄"
|
| 20 |
+
result = list(text)
|
| 21 |
+
for i in range(len(result)):
|
| 22 |
+
if rng.random() < intensity:
|
| 23 |
+
result[i] = rng.choice(glitch_chars)
|
| 24 |
+
return "".join(result)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def run_boot_sequence(console: Console, boot_lines: list[tuple[str, str]]) -> None:
|
| 28 |
+
"""Run the CRT boot sequence effect.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
console: Rich console instance.
|
| 32 |
+
boot_lines: List of (text, rich_style) tuples to display.
|
| 33 |
+
"""
|
| 34 |
+
term_height = min(console.height - 2, 40)
|
| 35 |
+
rng = random.Random(42)
|
| 36 |
+
|
| 37 |
+
with Live(console=console, refresh_per_second=30, transient=True) as live:
|
| 38 |
+
displayed_lines: list[tuple[str, str]] = []
|
| 39 |
+
|
| 40 |
+
for line_text, line_style in boot_lines:
|
| 41 |
+
if not line_text:
|
| 42 |
+
displayed_lines.append(("", ""))
|
| 43 |
+
continue
|
| 44 |
+
|
| 45 |
+
line_len = max(1, len(line_text))
|
| 46 |
+
# Type out each character
|
| 47 |
+
for char_idx in range(len(line_text) + 1):
|
| 48 |
+
result = Text()
|
| 49 |
+
progress = char_idx / line_len
|
| 50 |
+
noise = settle_curve(progress)
|
| 51 |
+
prev_glitch_chance = 0.01 + 0.06 * noise
|
| 52 |
+
prev_glitch_intensity = 0.02 + 0.12 * noise
|
| 53 |
+
scanline_chance = 0.005 + 0.03 * noise
|
| 54 |
+
|
| 55 |
+
# Render previously completed lines
|
| 56 |
+
for prev_text, prev_style in displayed_lines:
|
| 57 |
+
if rng.random() < prev_glitch_chance:
|
| 58 |
+
result.append(_glitch_text(prev_text, prev_glitch_intensity, rng), style=prev_style)
|
| 59 |
+
else:
|
| 60 |
+
result.append(prev_text, style=prev_style)
|
| 61 |
+
result.append("\n")
|
| 62 |
+
|
| 63 |
+
# Current line being typed
|
| 64 |
+
typed = line_text[:char_idx]
|
| 65 |
+
cursor = "█" if char_idx < len(line_text) else ""
|
| 66 |
+
|
| 67 |
+
# Noise after cursor
|
| 68 |
+
noise_tail = ""
|
| 69 |
+
if char_idx < len(line_text):
|
| 70 |
+
noise_len = rng.randint(0, int(1 + 5 * noise))
|
| 71 |
+
noise_tail = "".join(rng.choice("░▒▓") for _ in range(noise_len))
|
| 72 |
+
|
| 73 |
+
result.append(typed, style=line_style)
|
| 74 |
+
result.append(cursor, style="bold rgb(255,200,80)")
|
| 75 |
+
result.append(noise_tail, style="dim rgb(180,140,40)")
|
| 76 |
+
result.append("\n")
|
| 77 |
+
|
| 78 |
+
# Faint scanlines in remaining space
|
| 79 |
+
remaining = term_height - len(displayed_lines) - 2
|
| 80 |
+
for _ in range(max(0, remaining)):
|
| 81 |
+
if rng.random() < scanline_chance:
|
| 82 |
+
scan_len = rng.randint(5, 30)
|
| 83 |
+
result.append("─" * scan_len, style="dim rgb(180,140,40)")
|
| 84 |
+
result.append("\n")
|
| 85 |
+
|
| 86 |
+
live.update(result)
|
| 87 |
+
|
| 88 |
+
# Variable typing speed
|
| 89 |
+
if line_text[char_idx - 1:char_idx] in " .":
|
| 90 |
+
time.sleep(0.025)
|
| 91 |
+
else:
|
| 92 |
+
time.sleep(0.010)
|
| 93 |
+
|
| 94 |
+
displayed_lines.append((line_text, line_style))
|
| 95 |
+
time.sleep(0.06)
|
| 96 |
+
|
| 97 |
+
# Hold with blinking cursor
|
| 98 |
+
for frame in range(20):
|
| 99 |
+
result = Text()
|
| 100 |
+
for prev_text, prev_style in displayed_lines:
|
| 101 |
+
result.append(prev_text, style=prev_style)
|
| 102 |
+
result.append("\n")
|
| 103 |
+
if frame % 8 < 4:
|
| 104 |
+
result.append("█", style="rgb(255,200,80)")
|
| 105 |
+
live.update(result)
|
| 106 |
+
time.sleep(0.05)
|
| 107 |
+
|
| 108 |
+
# Print final clean frame
|
| 109 |
+
final = Text()
|
| 110 |
+
for prev_text, prev_style in displayed_lines:
|
| 111 |
+
final.append(prev_text, style=prev_style)
|
| 112 |
+
final.append("\n")
|
| 113 |
+
console.print(final)
|
agent/utils/particle_logo.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Particle coalesce effect for the HUGGING FACE AGENT logo.
|
| 2 |
+
|
| 3 |
+
Random particles swirl in from the edges, converge to form the text
|
| 4 |
+
"HUGGING FACE AGENT", hold briefly, then the final frame is printed.
|
| 5 |
+
Rendered with braille characters for high detail.
|
| 6 |
+
|
| 7 |
+
Based on Leandro's particle_coalesce.py demo.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
import random
|
| 12 |
+
import time
|
| 13 |
+
|
| 14 |
+
from rich.console import Console
|
| 15 |
+
from rich.text import Text
|
| 16 |
+
from rich.align import Align
|
| 17 |
+
from rich.live import Live
|
| 18 |
+
|
| 19 |
+
from agent.utils.braille import BrailleCanvas, text_to_pixels
|
| 20 |
+
from agent.utils.boot_timing import settle_curve, warm_gold_from_white
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class Particle:
|
| 24 |
+
__slots__ = ("x", "y", "target_x", "target_y", "vx", "vy", "phase", "delay")
|
| 25 |
+
|
| 26 |
+
def __init__(self, x: float, y: float, target_x: float, target_y: float, delay: float = 0):
|
| 27 |
+
self.x = x
|
| 28 |
+
self.y = y
|
| 29 |
+
self.target_x = target_x
|
| 30 |
+
self.target_y = target_y
|
| 31 |
+
self.vx = 0.0
|
| 32 |
+
self.vy = 0.0
|
| 33 |
+
self.phase = random.uniform(0, math.pi * 2)
|
| 34 |
+
self.delay = delay
|
| 35 |
+
|
| 36 |
+
def update_converge(self, t: float, strength: float = 0.08, damping: float = 0.92):
|
| 37 |
+
"""Move toward target with spring-like physics."""
|
| 38 |
+
if t < self.delay:
|
| 39 |
+
# Still in swirl phase
|
| 40 |
+
self.x += self.vx
|
| 41 |
+
self.y += self.vy
|
| 42 |
+
self.vx *= 0.99
|
| 43 |
+
self.vy *= 0.99
|
| 44 |
+
# Gentle spiral
|
| 45 |
+
angle = self.phase + t * 2
|
| 46 |
+
self.vx += math.cos(angle) * 0.3
|
| 47 |
+
self.vy += math.sin(angle) * 0.3
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
# Spring toward target
|
| 51 |
+
dx = self.target_x - self.x
|
| 52 |
+
dy = self.target_y - self.y
|
| 53 |
+
self.vx += dx * strength
|
| 54 |
+
self.vy += dy * strength
|
| 55 |
+
self.vx *= damping
|
| 56 |
+
self.vy *= damping
|
| 57 |
+
self.x += self.vx
|
| 58 |
+
self.y += self.vy
|
| 59 |
+
|
| 60 |
+
@property
|
| 61 |
+
def at_target(self) -> bool:
|
| 62 |
+
return abs(self.x - self.target_x) < 1.5 and abs(self.y - self.target_y) < 1.5
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def run_particle_logo(console: Console, hold_seconds: float = 1.5) -> None:
|
| 66 |
+
"""Run the particle coalesce effect."""
|
| 67 |
+
term_width = min(console.width, 120)
|
| 68 |
+
term_height = min(console.height - 4, 35)
|
| 69 |
+
|
| 70 |
+
canvas = BrailleCanvas(term_width, term_height)
|
| 71 |
+
|
| 72 |
+
# Get target positions from text
|
| 73 |
+
text_pixels_line1 = text_to_pixels("HUGGING FACE", scale=2)
|
| 74 |
+
text_pixels_line2 = text_to_pixels("AGENT", scale=2)
|
| 75 |
+
|
| 76 |
+
# Calculate dimensions for centering
|
| 77 |
+
def get_bounds(pixels):
|
| 78 |
+
if not pixels:
|
| 79 |
+
return 0, 0, 0, 0
|
| 80 |
+
xs = [p[0] for p in pixels]
|
| 81 |
+
ys = [p[1] for p in pixels]
|
| 82 |
+
return min(xs), max(xs), min(ys), max(ys)
|
| 83 |
+
|
| 84 |
+
min_x1, max_x1, min_y1, max_y1 = get_bounds(text_pixels_line1)
|
| 85 |
+
min_x2, max_x2, min_y2, max_y2 = get_bounds(text_pixels_line2)
|
| 86 |
+
|
| 87 |
+
w1, h1 = max_x1 - min_x1 + 1, max_y1 - min_y1 + 1
|
| 88 |
+
w2, h2 = max_x2 - min_x2 + 1, max_y2 - min_y2 + 1
|
| 89 |
+
|
| 90 |
+
total_h = h1 + 6 + h2 # gap between lines
|
| 91 |
+
start_y = (canvas.pixel_height - total_h) // 2
|
| 92 |
+
|
| 93 |
+
# Center line 1
|
| 94 |
+
offset_x1 = (canvas.pixel_width - w1) // 2 - min_x1
|
| 95 |
+
offset_y1 = start_y - min_y1
|
| 96 |
+
targets_1 = [(p[0] + offset_x1, p[1] + offset_y1) for p in text_pixels_line1]
|
| 97 |
+
|
| 98 |
+
# Center line 2
|
| 99 |
+
offset_x2 = (canvas.pixel_width - w2) // 2 - min_x2
|
| 100 |
+
offset_y2 = start_y + h1 + 6 - min_y2
|
| 101 |
+
targets_2 = [(p[0] + offset_x2, p[1] + offset_y2) for p in text_pixels_line2]
|
| 102 |
+
|
| 103 |
+
all_targets = targets_1 + targets_2
|
| 104 |
+
|
| 105 |
+
# Subsample for performance — take every Nth pixel
|
| 106 |
+
step = max(1, len(all_targets) // 1500)
|
| 107 |
+
sampled_targets = all_targets[::step]
|
| 108 |
+
|
| 109 |
+
# Create particles at random edge positions
|
| 110 |
+
rng = random.Random(42)
|
| 111 |
+
particles = []
|
| 112 |
+
pw, ph = canvas.pixel_width, canvas.pixel_height
|
| 113 |
+
|
| 114 |
+
for i, (tx, ty) in enumerate(sampled_targets):
|
| 115 |
+
# Spawn from random edge
|
| 116 |
+
side = rng.choice(["top", "bottom", "left", "right"])
|
| 117 |
+
if side == "top":
|
| 118 |
+
sx, sy = rng.uniform(0, pw), rng.uniform(-20, -5)
|
| 119 |
+
elif side == "bottom":
|
| 120 |
+
sx, sy = rng.uniform(0, pw), rng.uniform(ph + 5, ph + 20)
|
| 121 |
+
elif side == "left":
|
| 122 |
+
sx, sy = rng.uniform(-20, -5), rng.uniform(0, ph)
|
| 123 |
+
else:
|
| 124 |
+
sx, sy = rng.uniform(pw + 5, pw + 20), rng.uniform(0, ph)
|
| 125 |
+
|
| 126 |
+
delay = rng.uniform(0, 0.4) # staggered start
|
| 127 |
+
p = Particle(sx, sy, tx, ty, delay=delay)
|
| 128 |
+
# Initial velocity — gentle swirl
|
| 129 |
+
angle = math.atan2(ph / 2 - sy, pw / 2 - sx) + rng.gauss(0, 0.8)
|
| 130 |
+
speed = rng.uniform(1.0, 2.5)
|
| 131 |
+
p.vx = math.cos(angle) * speed
|
| 132 |
+
p.vy = math.sin(angle) * speed
|
| 133 |
+
particles.append(p)
|
| 134 |
+
|
| 135 |
+
# Also add some extra ambient particles that never converge
|
| 136 |
+
ambient = []
|
| 137 |
+
for _ in range(200):
|
| 138 |
+
ax = rng.uniform(0, pw)
|
| 139 |
+
ay = rng.uniform(0, ph)
|
| 140 |
+
ap = Particle(ax, ay, ax, ay)
|
| 141 |
+
ap.vx = rng.gauss(0, 1)
|
| 142 |
+
ap.vy = rng.gauss(0, 1)
|
| 143 |
+
ambient.append(ap)
|
| 144 |
+
|
| 145 |
+
# Timing: 1s converge + 2s hold = 3s total
|
| 146 |
+
fps = 24
|
| 147 |
+
converge_frames = int(fps * 0.9)
|
| 148 |
+
hold_frames = int(fps * hold_seconds)
|
| 149 |
+
total_frames = converge_frames + hold_frames
|
| 150 |
+
|
| 151 |
+
with Live(console=console, refresh_per_second=fps, transient=True) as live:
|
| 152 |
+
for frame in range(total_frames):
|
| 153 |
+
canvas.clear()
|
| 154 |
+
t = frame * 0.03
|
| 155 |
+
|
| 156 |
+
# Update ambient particles (always drifting)
|
| 157 |
+
for ap in ambient:
|
| 158 |
+
ap.x += ap.vx + math.sin(t + ap.phase) * 0.5
|
| 159 |
+
ap.y += ap.vy + math.cos(t + ap.phase * 1.3) * 0.5
|
| 160 |
+
# Wrap around
|
| 161 |
+
ap.x = ap.x % pw
|
| 162 |
+
ap.y = ap.y % ph
|
| 163 |
+
|
| 164 |
+
# Fade out ambient during hold phase
|
| 165 |
+
if frame < converge_frames:
|
| 166 |
+
alpha = 0.3 + 0.2 * math.sin(t * 2 + ap.phase)
|
| 167 |
+
else:
|
| 168 |
+
fade = (frame - converge_frames) / hold_frames
|
| 169 |
+
alpha = (0.3 + 0.2 * math.sin(t * 2 + ap.phase)) * (1 - fade)
|
| 170 |
+
if alpha > 0.25:
|
| 171 |
+
canvas.set_pixel(int(ap.x), int(ap.y))
|
| 172 |
+
|
| 173 |
+
if frame < converge_frames:
|
| 174 |
+
# Converge phase
|
| 175 |
+
progress = frame / converge_frames
|
| 176 |
+
noise = settle_curve(progress)
|
| 177 |
+
for p in particles:
|
| 178 |
+
p.update_converge(t, strength=0.06, damping=0.90)
|
| 179 |
+
canvas.set_pixel(int(p.x), int(p.y))
|
| 180 |
+
|
| 181 |
+
# Trail effect
|
| 182 |
+
trail_scale = 0.2 + 0.5 * noise
|
| 183 |
+
trail_x = int(p.x - p.vx * trail_scale)
|
| 184 |
+
trail_y = int(p.y - p.vy * trail_scale)
|
| 185 |
+
canvas.set_pixel(trail_x, trail_y)
|
| 186 |
+
|
| 187 |
+
# Color transitions from white to warm gold
|
| 188 |
+
r, g, b = warm_gold_from_white(progress)
|
| 189 |
+
else:
|
| 190 |
+
# Hold phase — settle into solid logo
|
| 191 |
+
settle_t = (frame - converge_frames) / hold_frames
|
| 192 |
+
for p in particles:
|
| 193 |
+
# Jitter decays to zero
|
| 194 |
+
jitter = (1 - settle_t) * 0.7
|
| 195 |
+
jx = p.target_x + math.sin(t * 3 + p.phase) * jitter
|
| 196 |
+
jy = p.target_y + math.cos(t * 3 + p.phase * 1.5) * jitter
|
| 197 |
+
canvas.set_pixel(int(jx), int(jy))
|
| 198 |
+
canvas.set_pixel(int(p.target_x), int(p.target_y))
|
| 199 |
+
|
| 200 |
+
r, g, b = 255, 200, 80
|
| 201 |
+
|
| 202 |
+
# Render with color
|
| 203 |
+
lines = canvas.render()
|
| 204 |
+
result = Text()
|
| 205 |
+
for line in lines:
|
| 206 |
+
for ch in line:
|
| 207 |
+
if ch == chr(0x2800):
|
| 208 |
+
result.append(ch)
|
| 209 |
+
else:
|
| 210 |
+
result.append(ch, style=f"rgb({r},{g},{b})")
|
| 211 |
+
result.append("\n")
|
| 212 |
+
|
| 213 |
+
live.update(Align.center(result))
|
| 214 |
+
time.sleep(1.0 / fps)
|
| 215 |
+
|
| 216 |
+
# Print final settled frame
|
| 217 |
+
canvas.clear()
|
| 218 |
+
for p in particles:
|
| 219 |
+
canvas.set_pixel(int(p.target_x), int(p.target_y))
|
| 220 |
+
final = Text()
|
| 221 |
+
for line in canvas.render():
|
| 222 |
+
for ch in line:
|
| 223 |
+
if ch == chr(0x2800):
|
| 224 |
+
final.append(ch)
|
| 225 |
+
else:
|
| 226 |
+
final.append(ch, style="rgb(255,200,80)")
|
| 227 |
+
final.append("\n")
|
| 228 |
+
console.print(Align.center(final))
|