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.
- staticfx/__init__.py +3 -0
- staticfx/__main__.py +5 -0
- staticfx/app/__init__.py +1 -0
- staticfx/app/app.py +335 -0
- staticfx/app/keymap.py +163 -0
- staticfx/app/player.py +143 -0
- staticfx/app/session.py +97 -0
- staticfx/cli/__init__.py +20 -0
- staticfx/cli/commands.py +293 -0
- staticfx/cli/parser.py +185 -0
- staticfx/core/__init__.py +1 -0
- staticfx/core/clock.py +30 -0
- staticfx/core/color.py +89 -0
- staticfx/core/frame.py +85 -0
- staticfx/core/glyphs.py +15 -0
- staticfx/core/keys.py +116 -0
- staticfx/core/renderer.py +84 -0
- staticfx/core/terminal.py +148 -0
- staticfx/effects/__init__.py +1 -0
- staticfx/effects/base.py +88 -0
- staticfx/effects/builtin/__init__.py +203 -0
- staticfx/effects/builtin/fields.py +404 -0
- staticfx/effects/builtin/geometry.py +77 -0
- staticfx/effects/builtin/particles.py +623 -0
- staticfx/effects/builtin/shapes.py +255 -0
- staticfx/effects/builtin/signals.py +298 -0
- staticfx/effects/builtin/simulations.py +439 -0
- staticfx/effects/builtin/terminal.py +362 -0
- staticfx/effects/curated.py +16 -0
- staticfx/effects/engine/__init__.py +11 -0
- staticfx/effects/engine/effect.py +267 -0
- staticfx/effects/engine/filters.py +115 -0
- staticfx/effects/engine/generate.py +203 -0
- staticfx/effects/engine/layers.py +396 -0
- staticfx/effects/engine/recipe.py +215 -0
- staticfx/effects/engine/schema.py +87 -0
- staticfx/effects/engine/warps.py +203 -0
- staticfx/effects/registry.py +103 -0
- staticfx/effects/spec.py +51 -0
- staticfx/integrations.py +48 -0
- staticfx/library/__init__.py +1 -0
- staticfx/library/config.py +122 -0
- staticfx/library/effects.py +159 -0
- staticfx/library/files.py +44 -0
- staticfx/library/legacy.py +180 -0
- staticfx/library/looks.py +134 -0
- staticfx/postfx.py +78 -0
- staticfx/py.typed +0 -0
- staticfx/themes.py +162 -0
- staticfx/ui/__init__.py +1 -0
- staticfx/ui/browser.py +269 -0
- staticfx/ui/dialogs.py +95 -0
- staticfx/ui/help.py +99 -0
- staticfx/ui/pickers.py +221 -0
- staticfx/ui/play.py +213 -0
- staticfx/ui/screen.py +39 -0
- staticfx/ui/settings.py +262 -0
- staticfx/ui/studio.py +433 -0
- staticfx/ui/widgets.py +196 -0
- staticfx-2.0.0.dist-info/METADATA +164 -0
- staticfx-2.0.0.dist-info/RECORD +65 -0
- staticfx-2.0.0.dist-info/WHEEL +5 -0
- staticfx-2.0.0.dist-info/entry_points.txt +2 -0
- staticfx-2.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|