Skip to content

Commit 30f05f1

Browse files
committed
Smooth lines (2x supersample) + updated HUD in reel generator
Render at 2160×3840 and downsample with Lanczos for anti-aliased strokes. HUD now shows labeled PROGRESS / INK / PLOT TIME / POSITION columns instead of BED 01 header. https://claude.ai/code/session_01VhWRYXbuqapZ9YkvksroaS
1 parent 1106f48 commit 30f05f1

1 file changed

Lines changed: 90 additions & 56 deletions

File tree

tools/gen_reel.py

Lines changed: 90 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030

3131
# ---------- config ----------
3232
W, H = 1080, 1920
33+
SS = 2 # supersample factor (render at SSx, downsample for AA)
34+
SW, SH = W*SS, H*SS
3335
PAPER = (246, 243, 236)
3436
INK_HEX = {'black': (23, 21, 15), 'red': (216, 52, 42), 'blue': (31, 74, 160)}
3537
HUD = (21, 22, 15)
3638
FEED = 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

3941
BOLD = '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf'
4042
REG = '/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

Comments
 (0)