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,115 @@
1
+ """Brightness filters (applied after layers are blended) and the blend modes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Callable
7
+
8
+ from staticfx.effects.engine.schema import Kind, Param, Params, registry
9
+
10
+ Filter = Callable[[float], float]
11
+ Field = Callable[[float, float], float]
12
+
13
+
14
+ def gamma(p: Params, t: float) -> Filter:
15
+ g = p["gamma"]
16
+ return lambda v: v**g
17
+
18
+
19
+ def posterize(p: Params, t: float) -> Filter:
20
+ n = p["levels"]
21
+ return lambda v: math.floor(v * n) / n
22
+
23
+
24
+ def contour(p: Params, t: float) -> Filter:
25
+ k, sharp = p["lines"] * math.pi, p["sharpness"]
26
+ return lambda v: abs(math.sin(v * k)) ** sharp
27
+
28
+
29
+ def invert(p: Params, t: float) -> Filter:
30
+ return lambda v: 1.0 - v
31
+
32
+
33
+ def cycle(p: Params, t: float) -> Filter:
34
+ offset = t * p["speed"]
35
+ return lambda v: (v + offset) % 1.0
36
+
37
+
38
+ def pulse(p: Params, t: float) -> Filter:
39
+ level = 0.7 + 0.3 * math.sin(t * p["rate"] * 3)
40
+ return lambda v: v * level
41
+
42
+
43
+ def fold(p: Params, t: float) -> Filter:
44
+ return lambda v: abs(2 * v - 1)
45
+
46
+
47
+ def sawtooth(p: Params, t: float) -> Filter:
48
+ n = p["repeats"]
49
+ return lambda v: (v * n) % 1.0
50
+
51
+
52
+ FILTERS = registry(
53
+ Kind(
54
+ "gamma",
55
+ "Gamma",
56
+ gamma,
57
+ (Param("gamma", "Gamma", 1.2, 0.3, 3.0, 0.1),),
58
+ summary="darken or lift the midtones",
59
+ ),
60
+ Kind(
61
+ "posterize",
62
+ "Posterize",
63
+ posterize,
64
+ (Param("levels", "Levels", 4, 2, 12, 1, integer=True),),
65
+ summary="flatten into a few bands",
66
+ ),
67
+ Kind(
68
+ "contour",
69
+ "Contour lines",
70
+ contour,
71
+ (
72
+ Param("lines", "Lines", 3, 1, 10, 1, integer=True),
73
+ Param("sharpness", "Sharpness", 1.0, 0.3, 3.0, 0.1),
74
+ ),
75
+ summary="draw lines along brightness levels",
76
+ ),
77
+ Kind("invert", "Invert", invert, summary="swap dark and bright"),
78
+ Kind(
79
+ "cycle",
80
+ "Cycle",
81
+ cycle,
82
+ (Param("speed", "Speed", 0.3, -1.0, 1.0, 0.05),),
83
+ summary="slide brightness around over time",
84
+ ),
85
+ Kind(
86
+ "pulse",
87
+ "Pulse",
88
+ pulse,
89
+ (Param("rate", "Rate", 0.3, -1.0, 1.0, 0.05),),
90
+ summary="fade in and out",
91
+ ),
92
+ Kind("fold", "Fold", fold, summary="brightest where it was mid-grey"),
93
+ Kind(
94
+ "sawtooth",
95
+ "Sawtooth",
96
+ sawtooth,
97
+ (Param("repeats", "Repeats", 4, 2, 10, 1, integer=True),),
98
+ summary="repeat the brightness range",
99
+ ),
100
+ )
101
+
102
+
103
+ # A blend combines the value so far with the next layer, sampled at (x, y).
104
+ Blend = Callable[[float, Field, float, float], float]
105
+
106
+ BLENDS: dict[str, Blend] = {
107
+ "average": lambda a, g, x, y: (a + g(x, y)) * 0.5,
108
+ "multiply": lambda a, g, x, y: a * g(x, y) * 1.4,
109
+ "lighten": lambda a, g, x, y: max(a, g(x, y)),
110
+ "darken": lambda a, g, x, y: min(a, g(x, y)),
111
+ "difference": lambda a, g, x, y: abs(a - g(x, y)),
112
+ "screen": lambda a, g, x, y: 1.0 - (1.0 - a) * (1.0 - g(x, y)),
113
+ "wrap": lambda a, g, x, y: (a + g(x, y)) % 1.0,
114
+ "displace": lambda a, g, x, y: g(x + (a - 0.5) * 0.35, y + (a - 0.5) * 0.35),
115
+ }
@@ -0,0 +1,203 @@
1
+ """Seeded recipe generation: ``generate(42)`` is the same effect forever.
2
+
3
+ The order of random draws below is load-bearing: changing it changes every
4
+ generated effect, which breaks people's favourites. Add new draws at the
5
+ end, never in the middle.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import random
11
+
12
+ from staticfx.core.color import hsv
13
+ from staticfx.effects.engine.filters import BLENDS, FILTERS
14
+ from staticfx.effects.engine.layers import LAYERS, MANDELBROT_SPOTS
15
+ from staticfx.effects.engine.recipe import CHARSETS, Palette, Recipe, Stage
16
+ from staticfx.effects.engine.warps import WARPS
17
+
18
+ GENERATED_COUNT = 1000
19
+
20
+ ADJECTIVES = (
21
+ "Quantum Neon Hyper Cosmic Liquid Fractal Crystal Solar Void Neural Plasma Cyber Astral Chrome"
22
+ " Ghost Ionic Prism Turbo Zen Obsidian Electric Velvet Atomic Lunar Sonic Arctic Molten Digital"
23
+ " Spectral Infinite Phantom Radiant Cryo Nova Titan Vapor Rogue Mythic Binary Ultra"
24
+ ).split()
25
+
26
+
27
+ def _warp(r: random.Random) -> Stage:
28
+ kind = r.choice(list(WARPS))
29
+ v = {
30
+ "rate": r.uniform(-0.6, 0.6),
31
+ "base": r.uniform(0.8, 2.4),
32
+ "amp": r.uniform(0.1, 0.5),
33
+ "zrate": r.uniform(0.3, 1.5),
34
+ "n": r.choice([3, 4, 5, 6, 7, 8, 10, 12]),
35
+ "rot": r.uniform(-0.4, 0.4),
36
+ "mode": r.choice(["xy", "x", "y"]),
37
+ "s": r.uniform(1.2, 4.0),
38
+ "dx": r.uniform(-0.5, 0.5),
39
+ "dy": r.uniform(-0.5, 0.5),
40
+ "k": r.uniform(-4, 4),
41
+ "freq": r.uniform(1.5, 6.0),
42
+ "c": r.uniform(0.15, 0.6),
43
+ "ang": r.uniform(0.3, 1.2),
44
+ "iters": r.choice([1, 2, 3]),
45
+ "q": r.uniform(4, 14),
46
+ "rfreq": r.uniform(5, 18),
47
+ "rrate": r.uniform(1, 4),
48
+ }
49
+ return Stage(kind, warp_params(kind, v))
50
+
51
+
52
+ def warp_params(kind: str, v: dict) -> dict:
53
+ """Map original-format warp values (``rate``, ``base``, ...) onto ``kind``'s params."""
54
+ params = {
55
+ "rotate": {"spin": v["rate"]},
56
+ "zoom": {"zoom": v["base"], "pulse": v["amp"], "pulse_rate": v["zrate"]},
57
+ "kaleidoscope": {"segments": v["n"], "spin": v["rot"]},
58
+ "mirror": {"axis": v["mode"]},
59
+ "tile": {"tiles": v["s"], "drift_x": v["dx"], "drift_y": v["dy"]},
60
+ "swirl": {"twist": v["k"]},
61
+ "wave": {"strength": v["amp"], "frequency": v["freq"]},
62
+ "polar": {},
63
+ "fold": {"offset": v["c"], "angle": v["ang"], "folds": v["iters"]},
64
+ "pixelate": {"resolution": v["q"]},
65
+ "ripple": {"strength": v["amp"], "frequency": v["rfreq"], "speed": v["rrate"]},
66
+ }[kind]
67
+ return WARPS[kind].clean(params)
68
+
69
+
70
+ def _layer(r: random.Random) -> Stage:
71
+ kind = r.choice(list(LAYERS))
72
+ speed = r.uniform(0.3, 2.0) * r.choice([-1, 1])
73
+ v = {
74
+ "f": r.uniform(2, 14),
75
+ "f2": r.uniform(2, 14),
76
+ "n": r.choice([2, 3, 4, 5, 6, 8, 12]),
77
+ "k": r.choice([3, 4, 5, 6, 7]),
78
+ "metric": r.choice(["circle", "diamond", "square", "star"]),
79
+ "shape": r.choice(["box", "cross", "star", "triangle", "ring"]),
80
+ "mode": r.choice(["edges", "distance", "cells"]),
81
+ "cre": r.uniform(-0.85, -0.3),
82
+ "cim": r.uniform(-0.6, 0.6),
83
+ "spot": r.choice(list(MANDELBROT_SPOTS)),
84
+ "seed": r.randrange(1 << 20),
85
+ }
86
+ return Stage(kind, layer_params(kind, v, speed))
87
+
88
+
89
+ def layer_params(kind: str, v: dict, speed: float) -> dict:
90
+ """Map original-format layer values (``f``, ``n``, ``k``, ...) onto ``kind``'s params."""
91
+ params = {
92
+ "grid": {"scale": v["f"], "scale_y": v["f2"]},
93
+ "rings": {"scale": v["f"], "shape": v["metric"]},
94
+ "spiral": {"scale": v["f"], "arms": v["n"]},
95
+ "rays": {"scale": v["f2"], "arms": v["n"]},
96
+ "checker": {"scale": v["f"], "scale_y": v["f2"]},
97
+ "moire": {"scale": v["f"]},
98
+ "flow": {"scale": v["f"]},
99
+ "blobs": {"count": v["k"]},
100
+ "julia": {"c_real": v["cre"], "c_imag": v["cim"]},
101
+ "mandelbrot": {"spot": v["spot"]},
102
+ "voronoi": {"scale": v["f"], "count": v["k"], "style": v["mode"]},
103
+ "hexagons": {"scale": v["f"]},
104
+ "lissajous": {"scale": v["f"], "x_waves": v["k"], "y_waves": v["n"]},
105
+ "tunnel": {"scale": v["f"], "arms": v["n"]},
106
+ "sources": {"scale": v["f"], "count": v["k"]},
107
+ "shape": {"scale": v["f"], "shape": v["shape"], "points": v["n"]},
108
+ "noise": {"scale": v["f"], "seed": v["seed"]},
109
+ }[kind]
110
+ return LAYERS[kind].clean({**params, "speed": speed})
111
+
112
+
113
+ def _filter(r: random.Random) -> Stage:
114
+ kind = r.choice(list(FILTERS))
115
+ v = {
116
+ "g": r.uniform(0.5, 2.2),
117
+ "n": r.choice([3, 4, 5, 6, 8]),
118
+ "k": r.choice([2, 3, 4, 6, 8]),
119
+ "pw": r.uniform(0.4, 2.5),
120
+ "c": r.uniform(-0.6, 0.6) or 0.3,
121
+ }
122
+ return Stage(kind, filter_params(kind, v))
123
+
124
+
125
+ def filter_params(kind: str, v: dict) -> dict:
126
+ """Map original-format filter values (``g``, ``n``, ``pw``, ...) onto ``kind``'s params."""
127
+ params = {
128
+ "gamma": {"gamma": v["g"]},
129
+ "posterize": {"levels": v["n"]},
130
+ "contour": {"lines": v["k"], "sharpness": v["pw"]},
131
+ "invert": {},
132
+ "cycle": {"speed": v["c"]},
133
+ "pulse": {"rate": v["c"]},
134
+ "fold": {},
135
+ "sawtooth": {"repeats": v["n"]},
136
+ }[kind]
137
+ return FILTERS[kind].clean(params)
138
+
139
+
140
+ def generate(seed: int) -> tuple[Recipe, str]:
141
+ """The recipe for generated effect number ``seed`` and its display name."""
142
+ r = random.Random((seed * 2654435761 + 977) % 2**32)
143
+ speed = r.uniform(0.35, 1.9)
144
+ adjective = ADJECTIVES[r.randrange(len(ADJECTIVES))]
145
+ noun_index = r.randrange(3)
146
+
147
+ warps = [_warp(r) for _ in range(r.choices([0, 1, 2, 3], [2, 5, 4, 1])[0])]
148
+ layers = [_layer(r) for _ in range(r.choices([1, 2, 3], [3, 5, 2])[0])]
149
+ for layer in layers[1:]:
150
+ layer.blend = r.choice(list(BLENDS))
151
+ filters = [_filter(r) for _ in range(r.choices([0, 1, 2], [3, 5, 2])[0])]
152
+
153
+ glyph_rain = r.random() < 0.12
154
+ charset = CHARSETS[r.randrange(1000) % len(CHARSETS)]
155
+ dither = r.random() < 0.35
156
+ roll = r.random()
157
+ if roll < 0.72:
158
+ palette = Palette("theme")
159
+ elif roll < 0.84:
160
+ palette = Palette("rainbow", r.uniform(0.3, 1.6), r.uniform(-0.3, 0.3), r.random())
161
+ elif roll < 0.93:
162
+ palette = Palette("duo", first=hsv(r.random()), second=hsv(r.random(), 0.9))
163
+ else:
164
+ palette = Palette("angular")
165
+ trail = r.uniform(0.72, 0.94) if r.random() < 0.3 else 0.0
166
+ scanlines = r.random() < 0.2
167
+ glitch = r.random() < 0.15
168
+ sparkle = r.uniform(0.002, 0.008) if r.random() < 0.2 else 0.0
169
+ pulse_on = r.random() < 0.15
170
+ pulse = r.uniform(1.0, 4.0) if pulse_on else 0.0
171
+
172
+ recipe = Recipe(
173
+ layers=layers,
174
+ warps=warps,
175
+ filters=filters,
176
+ speed=speed,
177
+ charset=charset,
178
+ glyph_rain=glyph_rain,
179
+ dither=dither,
180
+ palette=palette,
181
+ trail=trail,
182
+ pulse=pulse,
183
+ scanlines=scanlines,
184
+ glitch=glitch,
185
+ sparkle=sparkle,
186
+ seed=seed,
187
+ )
188
+ if palette.kind == "rainbow":
189
+ adjective = "Prismatic"
190
+ noun = LAYERS[layers[0].kind].nouns[noun_index]
191
+ return recipe, f"{adjective} {noun} {seed:03d}"
192
+
193
+
194
+ def describe(recipe: Recipe) -> str:
195
+ """One line saying what a recipe is made of, e.g. 'spiral + voronoi, kaleidoscope warp'."""
196
+ parts = [" + ".join(LAYERS[s.kind].label.lower() for s in recipe.layers)]
197
+ if recipe.warps:
198
+ parts.append(", ".join(WARPS[s.kind].label.lower() for s in recipe.warps) + " warp")
199
+ if recipe.filters:
200
+ parts.append(", ".join(FILTERS[s.kind].label.lower() for s in recipe.filters))
201
+ if recipe.palette.kind != "theme":
202
+ parts.append(f"{recipe.palette.kind} colours")
203
+ return ", ".join(parts)