171 lines
4.5 KiB
Python
171 lines
4.5 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from math import cos, pi, sin
|
||
|
|
from typing import Sequence
|
||
|
|
|
||
|
|
from rich.console import Console, ConsoleOptions, RenderResult
|
||
|
|
from rich.segment import Segment
|
||
|
|
from rich.style import Style
|
||
|
|
|
||
|
|
from textual.color import Color, Gradient
|
||
|
|
|
||
|
|
|
||
|
|
class VerticalGradient:
|
||
|
|
"""Draw a vertical gradient."""
|
||
|
|
|
||
|
|
def __init__(self, color1: str, color2: str) -> None:
|
||
|
|
self._color1 = Color.parse(color1)
|
||
|
|
self._color2 = Color.parse(color2)
|
||
|
|
|
||
|
|
def __rich_console__(
|
||
|
|
self, console: Console, options: ConsoleOptions
|
||
|
|
) -> RenderResult:
|
||
|
|
width = options.max_width
|
||
|
|
height = options.height or options.max_height
|
||
|
|
color1 = self._color1
|
||
|
|
color2 = self._color2
|
||
|
|
default_color = Color(0, 0, 0).rich_color
|
||
|
|
from_color = Style.from_color
|
||
|
|
blend = color1.blend
|
||
|
|
rich_color1 = color1.rich_color
|
||
|
|
for y in range(height):
|
||
|
|
line_color = from_color(
|
||
|
|
default_color,
|
||
|
|
(
|
||
|
|
blend(color2, y / (height - 1)).rich_color
|
||
|
|
if height > 1
|
||
|
|
else rich_color1
|
||
|
|
),
|
||
|
|
)
|
||
|
|
yield Segment(f"{width * ' '}\n", line_color)
|
||
|
|
|
||
|
|
|
||
|
|
class LinearGradient:
|
||
|
|
"""Render a linear gradient with a rotation.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
angle: Angle of rotation in degrees.
|
||
|
|
stops: List of stop consisting of pairs of offset (between 0 and 1) and color.
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self, angle: float, stops: Sequence[tuple[float, Color | str]]
|
||
|
|
) -> None:
|
||
|
|
self.angle = angle
|
||
|
|
self._stops = [
|
||
|
|
(stop, Color.parse(color) if isinstance(color, str) else color)
|
||
|
|
for stop, color in stops
|
||
|
|
]
|
||
|
|
self._color_gradient = Gradient(*self._stops)
|
||
|
|
|
||
|
|
def __rich_console__(
|
||
|
|
self, console: Console, options: ConsoleOptions
|
||
|
|
) -> RenderResult:
|
||
|
|
width = options.max_width
|
||
|
|
height = options.height or options.max_height
|
||
|
|
|
||
|
|
angle_radians = -self.angle * pi / 180.0
|
||
|
|
sin_angle = sin(angle_radians)
|
||
|
|
cos_angle = cos(angle_radians)
|
||
|
|
|
||
|
|
center_x = width / 2
|
||
|
|
center_y = height
|
||
|
|
|
||
|
|
new_line = Segment.line()
|
||
|
|
|
||
|
|
_Segment = Segment
|
||
|
|
get_color = self._color_gradient.get_rich_color
|
||
|
|
from_color = Style.from_color
|
||
|
|
|
||
|
|
for line_y in range(height):
|
||
|
|
point_y = float(line_y) * 2 - center_y
|
||
|
|
point_x = 0 - center_x
|
||
|
|
|
||
|
|
x1 = (center_x + (point_x * cos_angle - point_y * sin_angle)) / width
|
||
|
|
x2 = (
|
||
|
|
center_x + (point_x * cos_angle - (point_y + 1.0) * sin_angle)
|
||
|
|
) / width
|
||
|
|
point_x = width - center_x
|
||
|
|
end_x1 = (center_x + (point_x * cos_angle - point_y * sin_angle)) / width
|
||
|
|
delta_x = (end_x1 - x1) / width
|
||
|
|
|
||
|
|
if abs(delta_x) < 0.0001:
|
||
|
|
# Special case for verticals
|
||
|
|
yield _Segment(
|
||
|
|
"▀" * width,
|
||
|
|
from_color(
|
||
|
|
get_color(x1),
|
||
|
|
get_color(x2),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
else:
|
||
|
|
yield from [
|
||
|
|
_Segment(
|
||
|
|
"▀",
|
||
|
|
from_color(
|
||
|
|
get_color(x1 + x * delta_x),
|
||
|
|
get_color(x2 + x * delta_x),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
for x in range(width)
|
||
|
|
]
|
||
|
|
|
||
|
|
yield new_line
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
from rich import print
|
||
|
|
|
||
|
|
COLORS = [
|
||
|
|
"#881177",
|
||
|
|
"#aa3355",
|
||
|
|
"#cc6666",
|
||
|
|
"#ee9944",
|
||
|
|
"#eedd00",
|
||
|
|
"#99dd55",
|
||
|
|
"#44dd88",
|
||
|
|
"#22ccbb",
|
||
|
|
"#00bbcc",
|
||
|
|
"#0099cc",
|
||
|
|
"#3366bb",
|
||
|
|
"#663399",
|
||
|
|
]
|
||
|
|
|
||
|
|
stops = [(i / (len(COLORS) - 1), Color.parse(c)) for i, c in enumerate(COLORS)]
|
||
|
|
|
||
|
|
print(LinearGradient(25, stops))
|
||
|
|
|
||
|
|
from time import time
|
||
|
|
|
||
|
|
from textual.app import App, ComposeResult
|
||
|
|
from textual.widgets import Static
|
||
|
|
|
||
|
|
class GradientApp(App):
|
||
|
|
CSS = """
|
||
|
|
Screen {
|
||
|
|
background: transparent;
|
||
|
|
align: center middle;
|
||
|
|
}
|
||
|
|
|
||
|
|
Static {
|
||
|
|
padding: 2 4;
|
||
|
|
background: $panel;
|
||
|
|
width: 50;
|
||
|
|
}
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
def compose(self) -> ComposeResult:
|
||
|
|
yield Static("Gradients are fast now :-) ")
|
||
|
|
|
||
|
|
def render(self):
|
||
|
|
return LinearGradient(time() * 90, stops)
|
||
|
|
|
||
|
|
def on_mount(self) -> None:
|
||
|
|
self.set_interval(1 / 30, self.refresh)
|
||
|
|
|
||
|
|
app = GradientApp()
|
||
|
|
app.run()
|