Skip to content

Commit

Permalink
It's SIXEL TIME! Let's gooooooo.......
Browse files Browse the repository at this point in the history
  • Loading branch information
salt-die committed Feb 6, 2025
1 parent c2ed6f1 commit ec5826a
Show file tree
Hide file tree
Showing 99 changed files with 4,800 additions and 2,790 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,3 @@
.vscode/
build/
dist/
# generated by setup.py
_char_widths.py
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ repos:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/MarcoGorelli/cython-lint
rev: v0.16.2
hooks:
- id: cython-lint
- id: double-quote-cython-strings
6 changes: 5 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
"inherited-members": True,
"ignore-module-all": True,
}
autodoc_mock_imports = ["batgrl._char_widths"]

# FIXME: May need to mock other some other pyx files...
# FIXME: Not sure if `batgrl.char_width` needs to be mocked
autodoc_mock_imports = ["batgrl.char_width"]

html_theme = "pydata_sphinx_theme"
html_sidebars = {"**": ["search-field", "sidebar-nav-bs"]}
html_theme_options = {
Expand Down
8 changes: 5 additions & 3 deletions examples/advanced/cloth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import numpy as np
from batgrl.app import App
from batgrl.colors import AWHITE, AColor
from batgrl.gadgets.graphics import Graphics, Size
from batgrl.gadgets.graphics import Graphics, Size, scale_geometry
from batgrl.gadgets.slider import Slider
from batgrl.gadgets.text import Text
from numpy.typing import NDArray
Expand Down Expand Up @@ -77,10 +77,10 @@ def make_mesh(size: Size) -> tuple[list[Node], list[Link]]:

class Cloth(Graphics):
def __init__(self, mesh_size: Size, scale=5, mesh_color: AColor = AWHITE, **kwargs):
super().__init__(**kwargs)
self.nodes, self.links = make_mesh(mesh_size)
self.scale = scale
self.mesh_color = mesh_color
super().__init__(**kwargs)
self.on_size()

def on_size(self):
Expand Down Expand Up @@ -109,7 +109,9 @@ def step(self):
def on_mouse(self, mouse_event):
if mouse_event.button != "left":
return False
mouse_pos = np.array(self.to_local(mouse_event.pos))
mouse_pos = np.array(
scale_geometry(self._blitter, self.to_local(mouse_event.pos))
)
for node in self.nodes:
force_direction = self.scale_pos(node.position) - mouse_pos
magnitude = np.linalg.norm(force_direction)
Expand Down
18 changes: 9 additions & 9 deletions examples/advanced/doom_fire.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from batgrl.app import App
from batgrl.colors import Color
from batgrl.gadgets.gadget import Gadget, clamp
from batgrl.gadgets.graphics import Graphics
from batgrl.gadgets.graphics import Graphics, scale_geometry
from batgrl.gadgets.slider import Slider
from batgrl.gadgets.text import Text, new_cell

Expand Down Expand Up @@ -50,7 +50,8 @@
[223, 223, 159, 255],
[239, 239, 199, 255],
[255, 255, 255, 255],
]
],
dtype=np.uint8,
)

MAX_STRENGTH = len(FIRE_PALETTE) - 1
Expand All @@ -61,12 +62,9 @@

class DoomFire(Graphics):
def __init__(self, fire_strength=MAX_STRENGTH, **kwargs):
self._fire_strength = fire_strength
super().__init__(**kwargs)

h, w = self.size
self._fire_values = np.zeros((2 * h, w), dtype=int)
self.fire_strength = fire_strength

def on_add(self):
super().on_add()
self._step_forever_task = asyncio.create_task(self._step_forever())
Expand All @@ -82,17 +80,19 @@ def fire_strength(self):
@fire_strength.setter
def fire_strength(self, fire_strength):
self._fire_strength = clamp(fire_strength, 0, MAX_STRENGTH)
_, w = scale_geometry(self._blitter, self._size)

np.clip(
self._fire_strength + np.random.randint(-3, 4, self.width),
self._fire_strength + np.random.randint(-3, 4, w),
0,
MAX_STRENGTH,
out=self._fire_values[-1],
)

def on_size(self):
h, w = self._size
self._fire_values = np.zeros((2 * h, w), dtype=int)
self._fire_values = np.zeros(
scale_geometry(self._blitter, self._size), dtype=int
)
self.fire_strength = self.fire_strength # Trigger `fire_strength.setter`
self.texture = FIRE_PALETTE[self._fire_values]

Expand Down
88 changes: 38 additions & 50 deletions examples/advanced/exploding_logo.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
from batgrl.app import App
from batgrl.colors import Color
from batgrl.figfont import FIGFont
from batgrl.gadgets.pane import Cell, Pane
from batgrl.gadgets.text_field import TextParticleField, particle_data_from_canvas
from batgrl.gadgets.pane import Cell, Pane, Point, Size
from batgrl.gadgets.text_field import TextParticleField
from batgrl.geometry.easings import in_exp


def make_logo():
Expand All @@ -28,46 +29,43 @@ def make_logo():


LOGO = make_logo()
HEIGHT, WIDTH = LOGO.shape
LOGO_SIZE = Size(*LOGO.shape)

POWER = 2
MAX_PARTICLE_SPEED = 10
FRICTION = 0.99

NCOLORS = 100
YELLOW = Color.from_hex("c4a219")
BLUE = Color.from_hex("070c25")
BLUE = Color.from_hex("123456")

COLOR_CHANGE_SPEED = 5
PERCENTS = tuple(np.linspace(0, 1, 30))
PERCENTS = [in_exp(p) for p in np.linspace(0, 1, 30)]


class PokeParticleField(TextParticleField):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._old_middle = 0, 0

def on_add(self):
super().on_add()
self._reset_task = asyncio.create_task(asyncio.sleep(0)) # dummy task
self._update_task = asyncio.create_task(self.update())
_origin = Point(0, 0)
_reset_task: asyncio.Task | None = None
_update_task: asyncio.Task | None = None

def on_remove(self):
super().on_remove()
self._reset_task.cancel()
self._update_task.cancel()
if self._reset_task is not None:
self._reset_task.cancel()
if self._update_task is not None:
self._update_task.cancel()

def on_size(self):
super().on_size()
oh, ow = self._old_middle
h = self.height // 2
w = self.width // 2
nh, nw = self._old_middle = h - HEIGHT // 2, w - WIDTH // 2

real_positions = self.particle_properties["real_positions"]
real_positions += nh - oh, nw - ow
self.particle_properties["original_positions"] += nh - oh, nw - ow
self.particle_positions[:] = real_positions.astype(int)
if self._reset_task is not None:
self._reset_task.cancel()
if self._update_task is not None:
self._update_task.cancel()
old_origin = self._origin
self._origin = self.center - LOGO_SIZE.center
dif = old_origin - self._origin
self.particle_properties["original_positions"] -= dif
self.particle_positions -= dif

def on_mouse(self, mouse_event):
if mouse_event.button == "left" and self.collides_point(mouse_event.pos):
Expand All @@ -81,17 +79,19 @@ def on_mouse(self, mouse_event):
POWER * relative_distances / distances_sq[:, None]
)

if self._update_task.done():
if self._reset_task is not None:
self._reset_task.cancel()
if self._update_task is None or self._update_task.done():
self._update_task = asyncio.create_task(self.update())

def on_key(self, key_event):
if key_event.key == "r" and self._reset_task.done():
if key_event.key == "r" and (
self._reset_task is None or self._reset_task.done()
):
self._reset_task = asyncio.create_task(self.reset())

async def update(self):
positions = self.particle_positions
real_positions = self.particle_properties["real_positions"]
velocities = self.particle_properties["velocities"]

while True:
Expand All @@ -102,9 +102,8 @@ async def update(self):
speed_mask = speeds > MAX_PARTICLE_SPEED
velocities[speed_mask] *= MAX_PARTICLE_SPEED / speeds[:, None][speed_mask]

real_positions += velocities
positions += velocities
velocities *= FRICTION
positions[:] = real_positions.astype(int)

# Boundary conditions
ys, xs = positions.T
Expand All @@ -130,43 +129,32 @@ async def update(self):
await asyncio.sleep(0)

async def reset(self):
self._update_task.cancel()
if self._update_task is not None:
self._update_task.cancel()
self.particle_properties["velocities"][:] = 0

pos = self.particle_positions
start = pos.copy()
end = self.particle_properties["original_positions"]
real = self.particle_properties["real_positions"]

for percent in PERCENTS:
percent_left = 1 - percent

real[:] = percent_left * start + percent * end
pos[:] = real.astype(int)

pos[:] = (1 - percent) * start + percent * end
await asyncio.sleep(0.03)
pos[:] = end


class ExplodingLogoApp(App):
async def on_start(self):
cell_arr = np.zeros_like(LOGO, dtype=Cell)
cell_arr["char"] = LOGO
cell_arr["fg_color"] = YELLOW
positions, cells = particle_data_from_canvas(cell_arr)

props = dict(
original_positions=positions.copy(),
real_positions=positions.astype(float),
velocities=np.zeros((len(positions), 2), dtype=float),
)

field = PokeParticleField(
size_hint={"height_hint": 1.0, "width_hint": 1.0},
particle_positions=positions,
particle_cells=cells,
particle_properties=props,
is_transparent=True,
size_hint={"height_hint": 1.0, "width_hint": 1.0}, is_transparent=True
)
field.particles_from_cells(cell_arr)
field.particle_properties = {
"original_positions": field.particle_positions.copy(),
"velocities": np.zeros((field.nparticles, 2), dtype=float),
}

# This background to show off field transparency.
bg = Pane(
Expand Down
Loading

0 comments on commit ec5826a

Please sign in to comment.