-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathflowfield_print.py
More file actions
84 lines (67 loc) · 3.04 KB
/
Copy pathflowfield_print.py
File metadata and controls
84 lines (67 loc) · 3.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python3
"""Print a flow-field drawing on the S01 thermal printer.
Hundreds of particles are released into an invisible vector field (built from
layered sine waves) and trace their paths, weaving into organic flowing strands.
Each seed is a different current.
Examples:
python flowfield_print.py # random field
python flowfield_print.py --particles 1200
python flowfield_print.py --seed 1234
python flowfield_print.py --no-print
"""
from __future__ import annotations
import argparse
from math import cos, pi, sin
from pathlib import Path
import random
from PIL import Image, ImageDraw
from print_common import ROOT, Card
SS = 2 # supersample
def render(particles: int, steps: int, target: int, rng: random.Random) -> Image.Image:
size = target * SS
img = Image.new("L", (size, size), 255)
d = ImageDraw.Draw(img)
fa, fb, fc = (rng.uniform(2, 5) for _ in range(3))
p1, p2, p3 = (rng.uniform(0, 2 * pi) for _ in range(3))
swirl = rng.uniform(0.4, 0.9)
step_len = size * 0.011
def angle(x, y):
u, v = x / size, y / size
return 2 * pi * swirl * (sin(u * fa + p1) + cos(v * fb + p2) + sin((u + v) * fc + p3))
for _ in range(particles):
x, y = rng.uniform(0, size), rng.uniform(0, size)
path = [(x, y)]
for _ in range(steps):
a = angle(x, y)
x += cos(a) * step_len
y += sin(a) * step_len
if not (0 <= x < size and 0 <= y < size):
break
path.append((x, y))
if len(path) > 1:
d.line(path, fill=0, width=SS, joint="curve")
return img.resize((target, target), Image.LANCZOS)
def main() -> int:
parser = argparse.ArgumentParser(description="Print a flow-field drawing on the S01 thermal printer.")
parser.add_argument("--particles", type=int, default=850, help="Number of particles (default: 850).")
parser.add_argument("--steps", type=int, default=55, help="Steps each particle takes (default: 55).")
parser.add_argument("--seed", type=int, default=None, help="Seed for a reproducible field.")
parser.add_argument("--out", type=Path, default=None)
parser.add_argument("--darkness", type=int, choices=range(1, 6), default=3)
parser.add_argument("--bottom-feed", type=int, default=24)
parser.add_argument("--no-print", action="store_true")
args = parser.parse_args()
if not 100 <= args.particles <= 4000:
parser.error("--particles must be 100-4000")
seed = args.seed if args.seed is not None else random.randrange(1, 100000)
rng = random.Random(seed)
print(f"Releasing {args.particles} particles into the field (seed {seed}) ...")
card = Card()
img = render(args.particles, args.steps, card.inner_w, rng)
card.title("FLOW FIELD")
card.gap(4).image(img, border=True)
card.footer("vector field", f"seed {seed}")
out = args.out or ROOT / f"flowfield_{seed}.png"
return card.finish(out, args.bottom_feed, args.darkness, do_print=not args.no_print)
if __name__ == "__main__":
raise SystemExit(main())