staticfx 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of staticfx might be problematic. Click here for more details.

Files changed (65) hide show
  1. staticfx/__init__.py +3 -0
  2. staticfx/__main__.py +5 -0
  3. staticfx/app/__init__.py +1 -0
  4. staticfx/app/app.py +335 -0
  5. staticfx/app/keymap.py +163 -0
  6. staticfx/app/player.py +143 -0
  7. staticfx/app/session.py +97 -0
  8. staticfx/cli/__init__.py +20 -0
  9. staticfx/cli/commands.py +293 -0
  10. staticfx/cli/parser.py +185 -0
  11. staticfx/core/__init__.py +1 -0
  12. staticfx/core/clock.py +30 -0
  13. staticfx/core/color.py +89 -0
  14. staticfx/core/frame.py +85 -0
  15. staticfx/core/glyphs.py +15 -0
  16. staticfx/core/keys.py +116 -0
  17. staticfx/core/renderer.py +84 -0
  18. staticfx/core/terminal.py +148 -0
  19. staticfx/effects/__init__.py +1 -0
  20. staticfx/effects/base.py +88 -0
  21. staticfx/effects/builtin/__init__.py +203 -0
  22. staticfx/effects/builtin/fields.py +404 -0
  23. staticfx/effects/builtin/geometry.py +77 -0
  24. staticfx/effects/builtin/particles.py +623 -0
  25. staticfx/effects/builtin/shapes.py +255 -0
  26. staticfx/effects/builtin/signals.py +298 -0
  27. staticfx/effects/builtin/simulations.py +439 -0
  28. staticfx/effects/builtin/terminal.py +362 -0
  29. staticfx/effects/curated.py +16 -0
  30. staticfx/effects/engine/__init__.py +11 -0
  31. staticfx/effects/engine/effect.py +267 -0
  32. staticfx/effects/engine/filters.py +115 -0
  33. staticfx/effects/engine/generate.py +203 -0
  34. staticfx/effects/engine/layers.py +396 -0
  35. staticfx/effects/engine/recipe.py +215 -0
  36. staticfx/effects/engine/schema.py +87 -0
  37. staticfx/effects/engine/warps.py +203 -0
  38. staticfx/effects/registry.py +103 -0
  39. staticfx/effects/spec.py +51 -0
  40. staticfx/integrations.py +48 -0
  41. staticfx/library/__init__.py +1 -0
  42. staticfx/library/config.py +122 -0
  43. staticfx/library/effects.py +159 -0
  44. staticfx/library/files.py +44 -0
  45. staticfx/library/legacy.py +180 -0
  46. staticfx/library/looks.py +134 -0
  47. staticfx/postfx.py +78 -0
  48. staticfx/py.typed +0 -0
  49. staticfx/themes.py +162 -0
  50. staticfx/ui/__init__.py +1 -0
  51. staticfx/ui/browser.py +269 -0
  52. staticfx/ui/dialogs.py +95 -0
  53. staticfx/ui/help.py +99 -0
  54. staticfx/ui/pickers.py +221 -0
  55. staticfx/ui/play.py +213 -0
  56. staticfx/ui/screen.py +39 -0
  57. staticfx/ui/settings.py +262 -0
  58. staticfx/ui/studio.py +433 -0
  59. staticfx/ui/widgets.py +196 -0
  60. staticfx-2.0.0.dist-info/METADATA +164 -0
  61. staticfx-2.0.0.dist-info/RECORD +65 -0
  62. staticfx-2.0.0.dist-info/WHEEL +5 -0
  63. staticfx-2.0.0.dist-info/entry_points.txt +2 -0
  64. staticfx-2.0.0.dist-info/licenses/LICENSE +21 -0
  65. staticfx-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,439 @@
1
+ """Grids that evolve step by step: cellular automata, Langton's ant, a maze, fire and smoke."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from collections import deque
7
+
8
+ from staticfx.core.frame import Frame
9
+ from staticfx.effects.base import Effect
10
+ from staticfx.themes import Theme
11
+
12
+ MAX_STEPS_PER_FRAME = 12
13
+
14
+
15
+ class GridSimulation(Effect):
16
+ """Fixed-timestep simulation over ``grid[y][x]``.
17
+
18
+ ``advance`` runs as many ``step`` calls as the elapsed time covers, capped
19
+ so a slow frame can't trigger a long catch-up burst.
20
+ """
21
+
22
+ interval = 0.1
23
+ empty: object = 0
24
+
25
+ def __init__(self, rng: random.Random, unicode: bool = False) -> None:
26
+ super().__init__(rng, unicode)
27
+ self.grid: list[list] = []
28
+ self.timer = 0.0
29
+
30
+ def on_resize(self) -> None:
31
+ self.grid = [[self.empty] * self.width for _ in range(self.height)]
32
+ self.timer = 0.0
33
+ self.seed_grid()
34
+
35
+ def seed_grid(self) -> None:
36
+ """Populate the freshly built grid."""
37
+
38
+ def step(self) -> None:
39
+ raise NotImplementedError
40
+
41
+ def advance(self, dt: float) -> None:
42
+ self.timer += dt
43
+ for _ in range(MAX_STEPS_PER_FRAME):
44
+ if self.timer < self.interval:
45
+ return
46
+ self.timer -= self.interval
47
+ self.step()
48
+ self.timer = 0.0
49
+
50
+
51
+ # Cellular automata
52
+ class LifeLike(GridSimulation):
53
+ """Birth/survival rule automaton (Conway's Life is B3/S23).
54
+
55
+ Cells fade out over a few steps (``trail``) so motion reads clearly, and
56
+ the board reseeds itself when it dies out or settles into a short loop.
57
+ """
58
+
59
+ interval = 0.09
60
+
61
+ def __init__(
62
+ self,
63
+ rng: random.Random,
64
+ unicode: bool = False,
65
+ birth: frozenset[int] | set[int] = frozenset({3}),
66
+ survive: frozenset[int] | set[int] = frozenset({2, 3}),
67
+ fill: float = 0.28,
68
+ interval: float | None = None,
69
+ trail: float = 0.82,
70
+ ) -> None:
71
+ super().__init__(rng, unicode)
72
+
73
+ self.birth = frozenset(birth)
74
+ self.survive = frozenset(survive)
75
+ self.fill = fill
76
+ self.trail = trail
77
+
78
+ if interval is not None:
79
+ self.interval = interval
80
+
81
+ self.glow: list[list[float]] = []
82
+ self.history: deque[int] = deque(maxlen=14)
83
+ self.hold = 0.0
84
+
85
+ def seed_grid(self) -> None:
86
+ fill = self.fill
87
+ self.grid = [
88
+ [1 if self.rng.random() < fill else 0 for _ in range(self.width)]
89
+ for _ in range(self.height)
90
+ ]
91
+ self.glow = [[float(c) for c in row] for row in self.grid]
92
+ self.history.clear()
93
+ self.hold = 0.0
94
+
95
+ def step(self) -> None:
96
+ width, height = self.width, self.height
97
+ grid = self.grid
98
+ birth, survive = self.birth, self.survive
99
+ trail = self.trail
100
+ new_grid: list[list[int]] = []
101
+ population = 0
102
+
103
+ for y in range(height):
104
+ above = grid[(y - 1) % height]
105
+ row = grid[y]
106
+ below = grid[(y + 1) % height]
107
+ glow_row = self.glow[y]
108
+ out = []
109
+
110
+ for x in range(width):
111
+ left = x - 1
112
+ right = (x + 1) % width
113
+ total = (
114
+ above[left]
115
+ + above[x]
116
+ + above[right]
117
+ + row[left]
118
+ + row[right]
119
+ + below[left]
120
+ + below[x]
121
+ + below[right]
122
+ )
123
+
124
+ alive = 1 if (total in survive if row[x] else total in birth) else 0
125
+ population += alive
126
+ out.append(alive)
127
+ glow_row[x] = 1.0 if alive else glow_row[x] * trail
128
+
129
+ new_grid.append(out)
130
+
131
+ self.grid = new_grid
132
+
133
+ signature = hash((population, tuple(map(tuple, new_grid[:: max(1, height // 6)]))))
134
+
135
+ if population == 0 or signature in self.history:
136
+ self.hold += self.interval
137
+ else:
138
+ self.hold = 0.0
139
+
140
+ self.history.append(signature)
141
+
142
+ if self.hold > 1.6 or population < width * height * 0.003:
143
+ self.seed_grid()
144
+
145
+ def render(self, frame: Frame, theme: Theme) -> None:
146
+ palette = self.palette(theme)
147
+ top = len(palette) - 1
148
+
149
+ for y, row in enumerate(self.glow):
150
+ for x, glow in enumerate(row):
151
+ if glow < 0.08:
152
+ continue
153
+
154
+ char = "#" if glow >= 0.99 else "+" if glow > 0.5 else "."
155
+ frame.put(x, y, char, palette[int((0.2 + 0.8 * glow) * top)])
156
+
157
+
158
+ class Rule30(GridSimulation):
159
+ interval = 0.05
160
+
161
+ def __init__(self, rng: random.Random, unicode: bool = False, rule: int = 30) -> None:
162
+ super().__init__(rng, unicode)
163
+ self.rule = rule & 0xFF
164
+
165
+ def seed_grid(self) -> None:
166
+ first = [0] * self.width
167
+ first[self.width // 2] = 1
168
+ self.grid = [first]
169
+
170
+ while len(self.grid) < self.height:
171
+ self.step()
172
+
173
+ def step(self) -> None:
174
+ previous = self.grid[-1]
175
+ width = len(previous)
176
+ rule = self.rule
177
+
178
+ row = [
179
+ (rule >> (previous[i - 1] << 2 | previous[i] << 1 | previous[(i + 1) % width])) & 1
180
+ for i in range(width)
181
+ ]
182
+
183
+ self.grid.append(row)
184
+
185
+ if len(self.grid) > self.height:
186
+ self.grid.pop(0)
187
+
188
+ def render(self, frame: Frame, theme: Theme) -> None:
189
+ rows = len(self.grid)
190
+
191
+ for y, row in enumerate(self.grid):
192
+ recency = (y + 1) / rows
193
+ color = self.color_at(theme, 0.25 + 0.75 * recency)
194
+
195
+ for x, cell in enumerate(row):
196
+ if cell:
197
+ frame.put(x, y, "#", color)
198
+
199
+
200
+ class LangtonAnt(GridSimulation):
201
+ """Langton's ant, generalised to multi-colour turn rules (e.g. "RLLR")."""
202
+
203
+ interval = 0.03
204
+
205
+ DIRECTIONS = ((0, -1), (1, 0), (0, 1), (-1, 0))
206
+
207
+ def __init__(
208
+ self,
209
+ rng: random.Random,
210
+ unicode: bool = False,
211
+ rule: str = "RL",
212
+ ants: int = 1,
213
+ moves: int = 40,
214
+ ) -> None:
215
+ super().__init__(rng, unicode)
216
+
217
+ self.rule = rule
218
+ self.ant_count = ants
219
+ self.moves = moves
220
+ self.ants: list[list[int]] = []
221
+ self.total = 0
222
+
223
+ def seed_grid(self) -> None:
224
+ rng = self.rng
225
+ self.total = 0
226
+ self.ants = [
227
+ [
228
+ (self.width // 2 + (rng.randint(-6, 6) if self.ant_count > 1 else 0)) % self.width,
229
+ (self.height // 2 + (rng.randint(-3, 3) if self.ant_count > 1 else 0))
230
+ % self.height,
231
+ rng.randrange(4),
232
+ ]
233
+ for _ in range(self.ant_count)
234
+ ]
235
+
236
+ def step(self) -> None:
237
+ grid = self.grid
238
+ rule = self.rule
239
+ colors = len(rule)
240
+ width, height = self.width, self.height
241
+
242
+ for _ in range(self.moves):
243
+ for ant in self.ants:
244
+ x, y, heading = ant
245
+ value = grid[y][x]
246
+ heading = (heading + (1 if rule[value] == "R" else 3)) % 4
247
+ grid[y][x] = (value + 1) % colors
248
+ dx, dy = self.DIRECTIONS[heading]
249
+ ant[0] = (x + dx) % width
250
+ ant[1] = (y + dy) % height
251
+ ant[2] = heading
252
+
253
+ self.total += self.moves * len(self.ants)
254
+
255
+ if self.total > 220_000:
256
+ self.on_resize()
257
+
258
+ def render(self, frame: Frame, theme: Theme) -> None:
259
+ colors = len(self.rule)
260
+ glyphs = ".:+*#%@&"
261
+ palette = self.palette(theme)
262
+ top = len(palette) - 1
263
+
264
+ for y, row in enumerate(self.grid):
265
+ for x, value in enumerate(row):
266
+ if value:
267
+ tone = 0.3 + 0.55 * value / max(1, colors - 1)
268
+ frame.put(x, y, glyphs[min(value, len(glyphs) - 1)], palette[int(tone * top)])
269
+
270
+ for x, y, _ in self.ants:
271
+ frame.put(x, y, "@", theme.bright)
272
+
273
+
274
+ class Maze(GridSimulation):
275
+ """Depth-first maze that is carved live, then regenerated."""
276
+
277
+ interval = 0.012
278
+
279
+ def __init__(self, rng: random.Random, unicode: bool = False) -> None:
280
+ super().__init__(rng, unicode)
281
+ self.stack: list[tuple[int, int]] = []
282
+ self.finished = 0.0
283
+
284
+ empty = 1 # wall
285
+
286
+ def seed_grid(self) -> None:
287
+ self.stack = []
288
+ self.finished = 0.0
289
+
290
+ if self.width >= 5 and self.height >= 5:
291
+ self.grid[1][1] = 0
292
+ self.stack.append((1, 1))
293
+
294
+ def step(self) -> None:
295
+ if not self.stack:
296
+ self.finished += self.interval * 6
297
+
298
+ if self.finished > 3.5 and self.width >= 5 and self.height >= 5:
299
+ self.on_resize()
300
+
301
+ return
302
+
303
+ rng = self.rng
304
+ grid = self.grid
305
+
306
+ for _ in range(3):
307
+ if not self.stack:
308
+ return
309
+
310
+ x, y = self.stack[-1]
311
+ options = [
312
+ (x + dx * 2, y + dy * 2, dx, dy)
313
+ for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1))
314
+ if 1 <= x + dx * 2 < self.width - 1
315
+ and 1 <= y + dy * 2 < self.height - 1
316
+ and grid[y + dy * 2][x + dx * 2]
317
+ ]
318
+
319
+ if options:
320
+ nx, ny, dx, dy = rng.choice(options)
321
+ grid[y + dy][x + dx] = 0
322
+ grid[ny][nx] = 0
323
+ self.stack.append((nx, ny))
324
+ else:
325
+ self.stack.pop()
326
+
327
+ def render(self, frame: Frame, theme: Theme) -> None:
328
+ wall = self.color_at(theme, 0.45)
329
+ char = "█" if self.unicode else "#"
330
+
331
+ for y, row in enumerate(self.grid):
332
+ for x, cell in enumerate(row):
333
+ if cell:
334
+ frame.put(x, y, char, wall)
335
+
336
+ if self.stack:
337
+ x, y = self.stack[-1]
338
+ frame.put(x, y, "@", theme.bright)
339
+
340
+
341
+ # Heat simulations (fire, smoke)
342
+ class Fire(GridSimulation):
343
+ empty = 0.0
344
+ interval = 0.04
345
+
346
+ shade = " .,:;+*xX#%@"
347
+
348
+ def step(self) -> None:
349
+ rng = self.rng
350
+ grid = self.grid
351
+ width, height = self.width, self.height
352
+ cooling = 3.6 / max(8, height)
353
+
354
+ bottom = grid[-1]
355
+
356
+ for x in range(width):
357
+ bottom[x] = rng.uniform(0.75, 1.0) if rng.random() > 0.06 else 0.2
358
+
359
+ for y in range(height - 1):
360
+ row = grid[y]
361
+ below = grid[y + 1]
362
+ below2 = grid[min(y + 2, height - 1)]
363
+
364
+ for x in range(width):
365
+ heat = (below[x - 1] + below[x] + below[(x + 1) % width] + below2[x]) * 0.25
366
+ row[x] = max(0.0, heat - rng.uniform(0.0, cooling))
367
+
368
+ def render(self, frame: Frame, theme: Theme) -> None:
369
+ palette = self.palette(theme)
370
+ top = len(palette) - 1
371
+ shade = self.shade
372
+ last = len(shade) - 1
373
+
374
+ for y, row in enumerate(self.grid):
375
+ for x, heat in enumerate(row):
376
+ index = int(heat * last + 0.5)
377
+
378
+ if index > 0:
379
+ frame.put(x, y, shade[index], palette[int(min(1.0, heat) * top)])
380
+
381
+
382
+ class Smoke(GridSimulation):
383
+ empty = 0.0
384
+ interval = 0.06
385
+
386
+ shade = " .:-=+*oO0"
387
+
388
+ def __init__(self, rng: random.Random, unicode: bool = False) -> None:
389
+ super().__init__(rng, unicode)
390
+ self.wind = 0
391
+
392
+ def step(self) -> None:
393
+ rng = self.rng
394
+ grid = self.grid
395
+ width, height = self.width, self.height
396
+
397
+ if rng.random() < 0.08:
398
+ self.wind = rng.choice((-1, 0, 0, 1))
399
+
400
+ bottom = grid[-1]
401
+
402
+ for x in range(width):
403
+ bottom[x] *= 0.5
404
+
405
+ if rng.random() < 0.55:
406
+ center = rng.randrange(width)
407
+
408
+ for dx in range(-rng.randint(1, 4), rng.randint(2, 5)):
409
+ bottom[(center + dx) % width] = rng.uniform(0.6, 0.95)
410
+
411
+ wind = self.wind
412
+
413
+ for y in range(height - 1):
414
+ row = grid[y]
415
+ below = grid[y + 1]
416
+
417
+ for x in range(width):
418
+ source = x + wind
419
+ mixed = (
420
+ below[(source - 1) % width]
421
+ + below[source % width]
422
+ + below[(source + 1) % width]
423
+ ) / 3.0
424
+ row[x] = mixed * 0.94 - 0.004
425
+
426
+ def render(self, frame: Frame, theme: Theme) -> None:
427
+ palette = self.palette(theme)
428
+ top = len(palette) - 1
429
+ shade = self.shade
430
+ last = len(shade) - 1
431
+
432
+ for y, row in enumerate(self.grid):
433
+ for x, density in enumerate(row):
434
+ index = int(density * last + 0.5)
435
+
436
+ if index > 0:
437
+ frame.put(
438
+ x, y, shade[index], palette[int((0.15 + 0.6 * min(1.0, density)) * top)]
439
+ )