3030
3131# ---------- config ----------
3232W , H = 1080 , 1920
33+ SS = 2 # supersample factor (render at SSx, downsample for AA)
34+ SW , SH = W * SS , H * SS
3335PAPER = (246 , 243 , 236 )
3436INK_HEX = {'black' : (23 , 21 , 15 ), 'red' : (216 , 52 , 42 ), 'blue' : (31 , 74 , 160 )}
3537HUD = (21 , 22 , 15 )
3638FEED = 1100
37- ART = (60 , 280 , 960 , 1300 ) # x,y,w,h
39+ ART = (60 , 280 , 960 , 1300 ) # x,y,w,h at 1x (scaled up internally)
3840
3941BOLD = '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf'
4042REG = '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf'
@@ -90,9 +92,10 @@ def render(key, color, dur=15, fps=30, out=None):
9092 segs , total_len = seg_lengths (subpaths )
9193
9294 ax , ay , aw , ah = ART
93- sc = min (aw / vbw , ah / vbh )
94- ox = ax + (aw - vbw * sc )/ 2
95- oy = ay + (ah - vbh * sc )/ 2
95+ s = SS
96+ sc = min (aw / vbw , ah / vbh ) * s
97+ ox = (ax * s ) + (aw * s - vbw * sc ) / 2
98+ oy = (ay * s ) + (ah * s - vbh * sc ) / 2
9699 def mx (x ): return ox + (x - vbx )* sc
97100 def my (y ): return oy + (y - vby )* sc
98101
@@ -104,17 +107,20 @@ def my(y): return oy + (y-vby)*sc
104107 plot_end = int (total_frames * 0.80 )
105108 hold_end = int (total_frames * 0.92 )
106109
107- # fonts
108- f_logo = ImageFont .truetype (BOLD , 56 )
109- f_bed = ImageFont .truetype (BOLD , 26 )
110- f_name = ImageFont .truetype (REG , 22 )
111- f_jp = ImageFont .truetype (JP , 26 )
112- f_stat = ImageFont .truetype (BOLD , 22 )
113- f_small = ImageFont .truetype (REG , 17 )
114- f_end_logo = ImageFont .truetype (BOLD , 88 )
115- f_end_name = ImageFont .truetype (BOLD , 32 )
116- f_end_jp = ImageFont .truetype (JP , 30 )
117- f_end_tag = ImageFont .truetype (REG , 20 )
110+ # fonts (at SS scale)
111+ f_code = ImageFont .truetype (BOLD , 26 * s )
112+ f_name = ImageFont .truetype (REG , 22 * s )
113+ f_jp = ImageFont .truetype (JP , 26 * s )
114+ f_stat = ImageFont .truetype (BOLD , 20 * s )
115+ f_label = ImageFont .truetype (REG , 15 * s )
116+ f_end_logo = ImageFont .truetype (BOLD , 88 * s )
117+ f_end_name = ImageFont .truetype (BOLD , 32 * s )
118+ f_end_jp = ImageFont .truetype (JP , 30 * s )
119+ f_end_tag = ImageFont .truetype (REG , 20 * s )
120+
121+ LM = 60 * s # left margin
122+ RM = 60 * s # right margin
123+ line_w = max (2 , 2 * s )
118124
119125 out = out or f'/tmp/{ key } -{ color } -reel.mp4'
120126 ff = imageio_ffmpeg .write_frames (
@@ -136,7 +142,7 @@ def hud_rgba(a=0.30): return HUD + (int(255*a),)
136142 prog = 1.0 ; phase = 'end'
137143 alpha = (f - hold_end )/ (total_frames - hold_end )
138144
139- img = Image .new ('RGB' , (W , H ), PAPER )
145+ img = Image .new ('RGB' , (SW , SH ), PAPER )
140146 d = ImageDraw .Draw (img , 'RGBA' )
141147
142148 # ---- strokes up to prog ----
@@ -145,58 +151,86 @@ def hud_rgba(a=0.30): return HUD + (int(255*a),)
145151 for (x0 ,y0 ,x1 ,y1 ,cs ,L ) in segs :
146152 if cs >= target : break
147153 if cs + L <= target :
148- d .line ([(mx (x0 ),my (y0 )),(mx (x1 ),my (y1 ))], fill = ink , width = 2 )
154+ d .line ([(mx (x0 ),my (y0 )),(mx (x1 ),my (y1 ))], fill = ink , width = line_w )
149155 pen = (x1 ,y1 )
150156 else :
151157 frac = (target - cs )/ L
152158 xe = x0 + (x1 - x0 )* frac ; ye = y0 + (y1 - y0 )* frac
153- d .line ([(mx (x0 ),my (y0 )),(mx (xe ),my (ye ))], fill = ink , width = 2 )
159+ d .line ([(mx (x0 ),my (y0 )),(mx (xe ),my (ye ))], fill = ink , width = line_w )
154160 pen = (xe ,ye )
155161 break
156162
157163 # ---- HUD ----
158164 hc = hud_rgba (0.30 )
159- d .text ((60 ,60 ), f'BED 01 · { suit ["code" ]} ' , font = f_bed , fill = hc )
160- lw = d .textlength ('PLOTFLOW*' , font = f_bed )
161- d .text ((W - 60 - lw ,60 ), 'PLOTFLOW*' , font = f_bed , fill = hc )
162- d .text ((60 ,96 ), suit ['name' ], font = f_name , fill = hc )
163- d .text ((60 ,122 ), suit .get ('jp' ,'' ), font = f_jp , fill = hc )
164- fr = f'F{ FEED } mm/min'
165- d .text ((W - 60 - d .textlength (fr ,font = f_small ),98 ), fr , font = f_small , fill = hc )
166-
167- bar_y = H - 160
168- d .rectangle ([60 ,bar_y ,W - 60 ,bar_y + 3 ], fill = HUD + (26 ,))
169- d .rectangle ([60 ,bar_y ,60 + (W - 120 )* prog ,bar_y + 3 ], fill = ink )
170- ink_drawn = (target * mm_per_unit )/ 1000
171- elapsed = total_sec * prog
172- d .text ((60 ,bar_y + 16 ), f'{ fmt (elapsed )} / { fmt (total_sec )} ' , font = f_stat , fill = hc )
173- pct = f'{ round (prog * 100 )} %'
174- d .text ((W / 2 - d .textlength (pct ,font = f_stat )/ 2 ,bar_y + 16 ), pct , font = f_stat , fill = hc )
175- inkstr = f'{ ink_drawn :.1f} m / { ink_total_m :.1f} m ink'
176- d .text ((W - 60 - d .textlength (inkstr ,font = f_stat ),bar_y + 16 ), inkstr , font = f_stat , fill = hc )
177165
166+ # top-left: code + name + jp
167+ d .text ((LM , 60 * s ), suit ['code' ], font = f_code , fill = hc )
168+ d .text ((LM , 96 * s ), suit ['name' ], font = f_name , fill = hc )
169+ d .text ((LM , 124 * s ), suit .get ('jp' ,'' ), font = f_jp , fill = hc )
170+
171+ # top-right: PLOTFLOW*
172+ lw = d .textlength ('PLOTFLOW*' , font = f_code )
173+ d .text ((SW - RM - lw , 60 * s ), 'PLOTFLOW*' , font = f_code , fill = hc )
174+
175+ # ---- bottom stats ----
176+ bar_y = SH - 180 * s
177+ d .rectangle ([LM , bar_y , SW - RM , bar_y + 3 * s ], fill = HUD + (26 ,))
178+ d .rectangle ([LM , bar_y , LM + (SW - LM - RM )* prog , bar_y + 3 * s ], fill = ink )
179+
180+ ink_drawn = (target * mm_per_unit ) / 1000
181+ elapsed = total_sec * prog
182+
183+ # row 1: labels
184+ r1y = bar_y + 18 * s
185+ d .text ((LM , r1y ), 'PROGRESS' , font = f_label , fill = hud_rgba (0.20 ))
186+ col2x = LM + 220 * s
187+ d .text ((col2x , r1y ), 'INK' , font = f_label , fill = hud_rgba (0.20 ))
188+ col3x = LM + 440 * s
189+ d .text ((col3x , r1y ), 'PLOT TIME' , font = f_label , fill = hud_rgba (0.20 ))
190+ col4x = LM + 700 * s
191+ d .text ((col4x , r1y ), 'POSITION' , font = f_label , fill = hud_rgba (0.20 ))
192+
193+ # row 2: values
194+ r2y = r1y + 22 * s
195+ d .text ((LM , r2y ), f'{ round (prog * 100 )} %' , font = f_stat , fill = hc )
196+ d .text ((col2x , r2y ), f'{ ink_drawn :.1f} / { ink_total_m :.1f} m' , font = f_stat , fill = hc )
197+ d .text ((col3x , r2y ), f'{ fmt (elapsed )} / { fmt (total_sec )} ' , font = f_stat , fill = hc )
198+ if pen :
199+ d .text ((col4x , r2y ), f'X { round (pen [0 ]* mm_per_unit )} Y { round (pen [1 ]* mm_per_unit )} ' , font = f_stat , fill = hc )
200+ else :
201+ d .text ((col4x , r2y ), '— —' , font = f_stat , fill = hud_rgba (0.15 ))
202+
203+ # pen head crosshair
178204 if pen and 0 < prog < 1 :
179- px ,py = mx (pen [0 ]),my (pen [1 ]); r = 14
180- d .ellipse ([px - r ,py - r ,px + r ,py + r ], outline = (232 ,53 ,31 ,160 ), width = 2 )
181- d .line ([px - r * 1.8 ,py ,px + r * 1.8 ,py ], fill = (232 ,53 ,31 ,160 ), width = 2 )
182- d .line ([px ,py - r * 1.8 ,px ,py + r * 1.8 ], fill = (232 ,53 ,31 ,160 ), width = 2 )
183- d .text ((60 ,bar_y + 44 ), f'X { round (pen [0 ]* mm_per_unit )} Y { round (pen [1 ]* mm_per_unit )} ' , font = f_small , fill = hc )
184-
185- # ---- end card crossfade ----
186- if phase == 'end' :
187- ov = Image .new ('RGBA' ,(W ,H ),(246 ,243 ,236 ,int (255 * min (1 ,alpha ))))
188- img = Image .alpha_composite (img .convert ('RGBA' ), ov ).convert ('RGB' )
189- if alpha > 0.15 :
190- a2 = min (1 ,(alpha - 0.15 )/ 0.6 )
191- d2 = ImageDraw .Draw (img ,'RGBA' )
205+ px , py = mx (pen [0 ]), my (pen [1 ])
206+ r = 14 * s
207+ d .ellipse ([px - r ,py - r ,px + r ,py + r ], outline = (232 ,53 ,31 ,153 ), width = max (2 ,2 * s ))
208+ d .line ([px - r * 1.8 ,py ,px + r * 1.8 ,py ], fill = (232 ,53 ,31 ,153 ), width = max (1 ,s ))
209+ d .line ([px ,py - r * 1.8 ,px ,py + r * 1.8 ], fill = (232 ,53 ,31 ,153 ), width = max (1 ,s ))
210+
211+ # ---- downsample from SSx to 1x ----
212+ out_img = img .resize ((W , H ), Image .LANCZOS )
213+
214+ # ---- end card crossfade (at 1x to avoid resizing text) ----
215+ if phase == 'end' :
216+ ov = Image .new ('RGBA' , (W ,H ), (246 ,243 ,236 , int (255 * min (1 ,alpha ))))
217+ out_img = Image .alpha_composite (out_img .convert ('RGBA' ), ov ).convert ('RGB' )
218+ if alpha > 0.15 :
219+ a2 = min (1 , (alpha - 0.15 )/ 0.6 )
220+ d2 = ImageDraw .Draw (out_img , 'RGBA' )
221+ f1_logo = ImageFont .truetype (BOLD , 88 )
222+ f1_name = ImageFont .truetype (BOLD , 32 )
223+ f1_jp = ImageFont .truetype (JP , 30 )
224+ f1_tag = ImageFont .truetype (REG , 20 )
192225 def ctext (y ,txt ,fnt ,col ):
193- w = d2 .textlength (txt ,font = fnt ); d2 .text ((W / 2 - w / 2 ,y ),txt ,font = fnt ,fill = col + (int (255 * a2 ),))
194- ctext (H / 2 - 90 ,'PLOTFLOW*' ,f_end_logo ,(21 ,22 ,15 ))
195- ctext (H / 2 + 10 ,f'{ suit ["code" ]} { suit ["name" ]} ' ,f_end_name ,(21 ,22 ,15 ))
196- ctext (H / 2 + 60 ,suit .get ('jp' ,'' ),f_end_jp ,(91 ,94 ,84 ))
197- ctext (H - 110 ,'DRAWN BY MACHINE · マシンドロー' ,f_end_tag ,(143 ,145 ,132 ))
198-
199- ff .send (np .asarray (img ))
226+ tw = d2 .textlength (txt ,font = fnt )
227+ d2 .text ((W / 2 - tw / 2 , y ), txt , font = fnt , fill = col + (int (255 * a2 ),))
228+ ctext (H // 2 - 90 , 'PLOTFLOW*' , f1_logo , (21 ,22 ,15 ))
229+ ctext (H // 2 + 10 , f'{ suit ["code" ]} { suit ["name" ]} ' , f1_name , (21 ,22 ,15 ))
230+ ctext (H // 2 + 60 , suit .get ('jp' ,'' ), f1_jp , (91 ,94 ,84 ))
231+ ctext (H - 110 , 'DRAWN BY MACHINE · マシンドロー' , f1_tag , (143 ,145 ,132 ))
232+
233+ ff .send (np .asarray (out_img ))
200234
201235 ff .close ()
202236 sz = os .path .getsize (out )/ 1024 / 1024
0 commit comments