1
1
#!/usr/bin/env python3
2
2
import subprocess
3
3
import argparse
4
+ import sys
4
5
import os
5
6
import re
6
7
8
+ sys .path .append (os .path .join (os .path .dirname (sys .argv [0 ]), "../../lib" ))
9
+ import visutil
10
+
7
11
SourcePos = (500 , 0 )
8
12
ExpectedGrainCount = 25000
9
13
@@ -23,40 +27,14 @@ def make_resting(self, grains):
23
27
24
28
class LandOfSand :
25
29
def __init__ (self , x0 ,y0 , x1 ,y1 , rate = 3 ):
26
- self .x0 , self .y0 = x0 , y0
27
- self .width = x1 - x0 + 1
28
- self .height = y1 - y0 + 1
29
- self .bmp = bytearray (self .width * self .height * 3 )
30
+ self .bmp = visutil .Bitmap ((x0 ,y0 ), (x1 ,y1 ))
30
31
self .rate = rate
31
32
self .timeout = rate
32
33
self .resting = 0
33
34
self .falling = []
34
35
35
36
def isempty (self , x ,y ):
36
- x -= self .x0
37
- y -= self .y0
38
- if (x < 0 ) or (y < 0 ) or (x >= self .width ) or (y >= self .height ):
39
- return False
40
- offset = 3 * (x + self .width * y )
41
- return not (self .bmp [offset ] or self .bmp [offset + 1 ] or self .bmp [offset + 2 ])
42
-
43
- def put (self , x ,y , color ):
44
- x -= self .x0
45
- y -= self .y0
46
- if (x < 0 ) or (y < 0 ) or (x >= self .width ) or (y >= self .height ):
47
- return
48
- offset = 3 * (x + self .width * y )
49
- self .bmp [offset ] = color [0 ]
50
- self .bmp [offset + 1 ] = color [1 ]
51
- self .bmp [offset + 2 ] = color [2 ]
52
-
53
- def tofile (self , f ):
54
- f .write (self .bmp )
55
-
56
- def save (self , filename ):
57
- with open (filename , "wb" ) as f :
58
- f .write (f"P6\n { self .width } { self .height } \n 255\n " .encode ())
59
- self .tofile (f )
37
+ return not any (self .bmp .get (x ,y ))
60
38
61
39
def step (self , n = 1 ):
62
40
for _ in range (n ):
@@ -73,14 +51,14 @@ def step(self, n=1):
73
51
break
74
52
# update bitmap and falling grain state
75
53
if keeps_falling :
76
- self .put (x , y , Colors .Empty )
54
+ self .bmp . put (x , y , Colors .Empty )
77
55
x , y = nx , ny
78
- self .put (x , y , Colors .Falling )
56
+ self .bmp . put (x , y , Colors .Falling )
79
57
self .falling [grain_index ] = (x , y )
80
58
grain_index += 1
81
59
else :
82
60
self .resting += 1
83
- self .put (x , y , Colors .make_resting (self .resting ))
61
+ self .bmp . put (x , y , Colors .make_resting (self .resting ))
84
62
del self .falling [grain_index ]
85
63
86
64
# produce new grain of sand at source
@@ -89,36 +67,28 @@ def step(self, n=1):
89
67
self .timeout = self .rate
90
68
if self .isempty (* SourcePos ):
91
69
self .falling .append (SourcePos )
92
- self .put (* SourcePos , Colors .Falling )
70
+ self .bmp . put (* SourcePos , Colors .Falling )
93
71
else :
94
72
return False # final state reached
95
73
return True
96
74
97
75
98
- if __name__ == "__main__" :
99
- parser = argparse .ArgumentParser ()
100
- parser .add_argument ("-i" , "--input" , metavar = "FILE" , default = "input.txt" ,
101
- help = "input file name" )
102
- parser .add_argument ("-o" , "--output" , metavar = "VIDEO.mp4" ,
103
- help = "save result into video file [default: show on screen]" )
104
- parser .add_argument ("-z" , "--zoom" , metavar = "N" , type = int , default = 4 ,
105
- help = "scaling factor [integer; default: %(default)s]" )
106
- parser .add_argument ("-r" , "--fps" , metavar = "FPS" , type = int , default = 30 ,
107
- help = "video speed in frames per second [default: %(default)s]" )
76
+ def extra_args (parser ):
108
77
parser .add_argument ("-d" , "--drop" , metavar = "N" , type = int , default = 3 ,
109
78
help = "sand grain drop rate in cycles per grain [default: %(default)s]" )
110
79
parser .add_argument ("-s" , "--dps" , metavar = "DPS" , type = float , default = 1 ,
111
80
help = "simulation speed in drops per second [default: %(default)s]" )
112
81
parser .add_argument ("-a" , "--accel" , metavar = "N" , type = float , default = 0.5 ,
113
82
help = "accelerate by N drops per second for every resting grain [default: %(default)s]" )
114
- parser .add_argument ("-q" , "--crf" , metavar = "CRF" , type = int , default = 26 ,
115
- help = "x64 CRF quality factor [default: %(default)s]" )
116
- args = parser .parse_args ()
117
- gif = args .output and (os .path .splitext (args .output )[- 1 ].strip ("." ).lower () == "gif" )
83
+
84
+
85
+ if __name__ == "__main__" :
86
+ vis = visutil .Visualizer (input_arg = True , extra_args = extra_args )
87
+ args = vis .args
118
88
119
89
lines = []
120
90
all_coords = [SourcePos ]
121
- for line in open (args .input ):
91
+ for line in open (vis .input ):
122
92
coords = [tuple (map (int , c )) for c in re .findall (r'(\d+),(\d+)' , line )]
123
93
all_coords .extend (coords )
124
94
lines .append (coords )
@@ -130,45 +100,30 @@ def step(self, n=1):
130
100
sim = LandOfSand (left ,0 , right ,bottom , rate = args .drop )
131
101
for line in lines :
132
102
x , y = line [0 ]
133
- sim .put (x , y , Colors .Rock )
103
+ sim .bmp . put (x , y , Colors .Rock )
134
104
for tx , ty in line :
135
105
while (x != tx ) or (y != ty ):
136
106
x += (x < tx ) - (x > tx )
137
107
y += (y < ty ) - (y > ty )
138
- sim .put (x , y , Colors .Rock )
108
+ sim .bmp . put (x , y , Colors .Rock )
139
109
140
- cpf = min (1 , int (args .dps * args .drop / args .fps ))
110
+ cpf = min (1 , int (args .dps * args .drop / vis .fps ))
141
111
while args .drop > 1 :
142
112
mod = cpf % args .drop
143
113
if mod and (mod <= args .drop / 2 ): break
144
114
cpf += 1
145
115
def get_real_dps (cpf_override = None ):
146
- return (cpf_override or cpf ) * args .fps / args .drop
116
+ return (cpf_override or cpf ) * vis .fps / args .drop
147
117
real_dps = get_real_dps ()
148
- print (f"image size: { sim .width } x{ sim .height } cells -> { sim .width * args .zoom } x{ sim .height * args .zoom } pixels" )
149
- print (f"timing: { cpf } cycle(s)/frame x { args .fps } frames/sec = { cpf * args .fps } cycles/sec = { args .drop } cycle(s)/drop x { real_dps :.1f} drops/second" )
150
-
151
- if args .output : cmdline = ["ffmpeg" , "-hide_banner" , "-y" ]
152
- else : cmdline = ["ffplay" , "-hide_banner" ]
153
- cmdline += ["-f" , "rawvideo" , "-video_size" , f"{ sim .width } x{ sim .height } " , "-pixel_format" , "rgb24" , "-framerate" , str (args .fps )]
154
- if args .output : cmdline += ["-i" ]
155
- cmdline += ["-" , "-vf" , f"scale={ sim .width * args .zoom } x{ sim .height * args .zoom } :flags=neighbor" ]
156
- if not args .output :
157
- cmdline += ["-window_title" , "AoC Sand Simulation" ]
158
- elif gif :
159
- cmdline [- 1 ] += ",split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=bayer"
160
- cmdline += ["-f" , "gif" , args .output ]
161
- else :
162
- cmdline [- 1 ] += ",format=yuv420p"
163
- cmdline += ["-c:v" , "libx264" , "-profile:v" , "main" , "-preset:v" , "veryslow" , "-tune:v" , "animation" , "-crf:v" , str (args .crf ), args .output ]
164
- print (os .getenv ("PS4" , "+ " ) + ' ' .join ((f'"{ a } "' if (' ' in a ) else a ) for a in cmdline ))
165
-
166
- out = subprocess .Popen (cmdline , stdin = subprocess .PIPE )
118
+ print (f"image size: { sim .bmp .width } x{ sim .bmp .height } cells -> { sim .bmp .width * args .zoom } x{ sim .bmp .height * args .zoom } pixels" )
119
+ print (f"timing: { cpf } cycle(s)/frame x { vis .fps } frames/sec = { cpf * vis .fps } cycles/sec = { args .drop } cycle(s)/drop x { real_dps :.1f} drops/second" )
120
+
121
+ vis .start (sim .bmp )
167
122
try :
168
- for _ in range (( args . fps // 2 ) if args . output else 1 ):
169
- sim . tofile ( out . stdin ) # generate 1/2 second of initial state (or 1 frame if just viewing )
123
+ # generate 1/2 second of initial state (or 1 frame if just viewing)
124
+ vis . write ( n = (( vis . fps // 2 ) if args . output else 1 ) )
170
125
while sim .step (cpf ):
171
- sim . tofile ( out . stdin )
126
+ vis . write ( )
172
127
expect_dps = args .dps + args .accel * sim .resting
173
128
speed_changed = False
174
129
while expect_dps >= get_real_dps (cpf + args .drop ):
@@ -177,16 +132,8 @@ def get_real_dps(cpf_override=None):
177
132
if speed_changed :
178
133
real_dps = get_real_dps ()
179
134
print (f"timing changed: { cpf } cycle(s)/frame -> { real_dps :.1f} drops/second" )
180
- for _ in range (args .fps * 2 ):
181
- sim .tofile (out .stdin ) # generate 2 second of final state
135
+ vis .write (n = vis .fps * 2 ) # generate 2 second of final state
182
136
except (EnvironmentError , KeyboardInterrupt ):
183
137
pass
184
- try :
185
- out .stdin .close ()
186
- except EnvironmentError :
187
- pass
188
- try :
189
- out .wait ()
190
- except EnvironmentError :
191
- pass
138
+ vis .stop ()
192
139
print (f"simulation ended at { sim .resting } resting grains" )
0 commit comments