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,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)
|