mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1202 lines
51 KiB
Python
1202 lines
51 KiB
Python
"""MobileGL intro video — paper-cutout style. `python video.py stills 3.0 25.5 ...` or `python video.py render`."""
|
||
import sys, json, time, subprocess
|
||
from multiprocessing import Pool
|
||
from paper import *
|
||
|
||
cache = functools.lru_cache(None)
|
||
|
||
|
||
# ================================================================ generic sprites
|
||
@cache
|
||
def spr_label(text, font=('fredoka', 40, 'SemiBold'), bg=WHITE, fg=INK, padx=30, pady=16, r=10,
|
||
maxw=None, rough=1.0, spacing=6, align='center'):
|
||
if maxw:
|
||
text = wrap(text, font, maxw)
|
||
tw, th = measure(text, font, spacing)
|
||
w, h = tw + 2 * padx, th + 2 * pady
|
||
a = Art(w, h, f'label|{text}|{font}|{bg}|{fg}')
|
||
a.rrect(0, 0, w, h, r, bg, rough=rough)
|
||
a.text(w / 2, h / 2, text, font, fg, spacing=spacing, align=align)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_tile(ch, color):
|
||
a = Art(160, 180, f'tile|{ch}|{color}')
|
||
a.poly([(0, 14), (14, 0), (114, 0), (160, 46), (160, 166), (146, 180), (14, 180), (0, 166)],
|
||
color, rough=0.8)
|
||
a.poly([(114, 0), (114, 46), (160, 46)], shade(color, 1.3), rough=0.4)
|
||
a.text(78, 94, ch, ('fredoka', 122, 'Bold'), WHITE, shade=True)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_confetti(kind, color, size):
|
||
a = Art(size, size, f'conf|{kind}|{color}|{size}')
|
||
if kind == 0:
|
||
a.poly([(0, size), (size / 2, 0), (size, size)], color, rough=0.5)
|
||
elif kind == 1:
|
||
a.circle(size / 2, size / 2, size / 2, color, rough=0.5)
|
||
elif kind == 2:
|
||
a.rrect(0, 0, size, size, 3, color, rough=0.5)
|
||
else:
|
||
a.poly(star_pts(size / 2, size / 2, size / 2, size / 4.5), color, rough=0.3)
|
||
return a.build(pad=16)
|
||
|
||
|
||
@cache
|
||
def spr_crane(color):
|
||
lt, dk, dd = shade(color, 1.25), shade(color, 0.8), shade(color, 0.62)
|
||
a = Art(330, 230, f'crane|{color}')
|
||
a.poly([(178, 128), (252, 26), (206, 140)], dd, rough=0.5) # far wing
|
||
a.poly([(28, 66), (142, 150), (118, 164)], dk, rough=0.5) # tail
|
||
a.poly([(110, 150), (170, 116), (234, 150), (170, 182)], color, rough=0.5) # body
|
||
a.poly([(110, 150), (170, 182), (156, 150)], dk, rough=0.4, shade=False)
|
||
a.poly([(214, 148), (292, 50), (230, 158)], dk, rough=0.4) # neck
|
||
a.poly([(292, 50), (324, 66), (286, 64)], dd, rough=0.3) # head
|
||
a.poly([(138, 142), (92, 10), (194, 134)], lt, rough=0.5) # near wing
|
||
a.poly([(138, 142), (92, 10), (150, 120)], color, rough=0.4, shade=False)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_plane(color):
|
||
a = Art(170, 96, f'plane|{color}')
|
||
a.poly([(0, 18), (170, 48), (52, 48)], shade(color, 1.25), rough=0.4)
|
||
a.poly([(52, 48), (170, 48), (72, 64)], shade(color, 0.65), rough=0.3)
|
||
a.poly([(52, 48), (170, 48), (22, 88)], color, rough=0.4)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_bubble(text, font=('mono', 32), bg=WHITE, fg=INK, tail='bl'):
|
||
tw, th = measure(text, font)
|
||
w, h = tw + 60, th + 40
|
||
a = Art(w, h + 34, f'bubble|{text}|{bg}|{tail}')
|
||
tp = [(40, h - 8), (92, h - 8), (28, h + 32)] if tail == 'bl' else \
|
||
[(w - 92, h - 8), (w - 40, h - 8), (w - 28, h + 32)]
|
||
a.poly(tp, bg, rough=0.6, shade=False)
|
||
a.rrect(0, 0, w, h, 22, bg, shade=False)
|
||
a.text(w / 2, h / 2, text, font, fg)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_appwin():
|
||
a = Art(560, 400, 'appwin')
|
||
a.rrect(0, 0, 560, 400, 22, DARK)
|
||
a.circle(34, 28, 9, CORAL, shade=False, rough=0.3)
|
||
a.circle(60, 28, 9, MUSTARD, shade=False, rough=0.3)
|
||
a.circle(86, 28, 9, GREEN, shade=False, rough=0.3)
|
||
a.text(300, 28, 'my_gl_game', ('hand', 30), (225, 222, 215))
|
||
a.rrect(14, 52, 532, 334, 10, SKY, rough=0.6)
|
||
a.circle(462, 118, 34, MUSTARD)
|
||
a.ellipse(170, 112, 52, 22, WHITE, rough=0.7)
|
||
a.ellipse(206, 98, 34, 22, WHITE, rough=0.7)
|
||
heights = [3, 3, 4, 4, 3, 2, 2, 3, 4, 5, 5, 4]
|
||
sky = [(14, 386)]
|
||
for i, hh in enumerate(heights):
|
||
top = 386 - hh * 34
|
||
sky += [(14 + 44 * i, top), (14 + 44 * (i + 1), top)]
|
||
sky += [(546, 386)]
|
||
a.poly(sky, DIRT, rough=0.5)
|
||
for i, hh in enumerate(heights):
|
||
top = 386 - hh * 34
|
||
a.rrect(14 + 44 * i, top, 44, 14, 2, GREEN, rough=0.4, shade=i % 2 == 0)
|
||
a.rrect(152, 160, 18, 76, 2, DIRT, rough=0.4) # voxel tree
|
||
a.rrect(120, 104, 82, 66, 4, shade(GREEN, 0.8), rough=0.5)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_phone(screen=BG_SKY, w=230, h=440):
|
||
a = Art(w, h, f'phone|{screen}|{w}')
|
||
a.rrect(0, 0, w, h, 34, DARK)
|
||
a.rrect(14, 46, w - 28, h - 100, 12, screen, rough=0.6)
|
||
a.rrect(w / 2 - 30, 20, 60, 10, 5, (30, 32, 38), shade=False)
|
||
a.circle(w / 2, h - 28, 14, (84, 88, 98), shade=False)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_tablet(screen=BG_MINT):
|
||
a = Art(400, 300, f'tablet|{screen}')
|
||
a.rrect(0, 0, 400, 300, 28, DARK)
|
||
a.rrect(40, 18, 320, 264, 10, screen, rough=0.6)
|
||
a.circle(20, 150, 8, (84, 88, 98), shade=False)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_laptop(screen=SKY, s=1.0):
|
||
a = Art(360, 240, f'laptop|{screen}')
|
||
a.rrect(30, 0, 300, 196, 14, DARK)
|
||
a.rrect(44, 14, 272, 166, 6, screen, rough=0.5)
|
||
a.poly([(0, 202), (360, 202), (338, 236), (22, 236)], GREY, rough=0.6)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_gpu(size=220, label='GPU'):
|
||
a = Art(size, size, f'gpu|{size}|{label}')
|
||
pins = []
|
||
n = 5
|
||
e0, e1 = size * 0.03, size * 0.2
|
||
for k in range(n):
|
||
o = size * (0.26 + 0.48 * k / (n - 1))
|
||
pins += [[(o, e0), (o, e1)], [(o, size - e0), (o, size - e1)],
|
||
[(e0, o), (e1, o)], [(size - e0, o), (size - e1, o)]]
|
||
a.lines(pins, size * 0.05, GREY, caps=False, tex=0.03, shade=False)
|
||
m1, m2 = size * 0.14, size * 0.25
|
||
a.rrect(m1, m1, size - 2 * m1, size - 2 * m1, size * 0.07, DARK)
|
||
a.rrect(m2, m2, size - 2 * m2, size - 2 * m2, size * 0.04, (84, 92, 108))
|
||
a.text(size / 2, size / 2, label, ('fredoka', int(size * 0.2), 'SemiBold'), WHITE)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_slab(text, w, color, h=110, size=52, fg=WHITE):
|
||
a = Art(w, h, f'slab|{text}|{w}|{color}')
|
||
a.rrect(0, 0, w, h, 18, color)
|
||
a.rrect(10, 10, w - 20, h - 20, 12, shade(color, 1.08), shade=False, rough=0.6)
|
||
a.text(w / 2, h / 2, text, ('fredoka', size, 'SemiBold'), fg)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_arrow(L=120, color=INK, wd=22, head=46):
|
||
a = Art(L, head, f'arrow|{L}|{color}|{wd}|{head}')
|
||
cy, hx = head / 2, L - head * 0.9
|
||
a.poly([(0, cy - wd / 2), (hx, cy - wd / 2), (hx, 0), (L, cy), (hx, head), (hx, cy + wd / 2),
|
||
(0, cy + wd / 2)], color, rough=0.6)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_arrow_half(side):
|
||
rng = random.Random(5 if side == 'L' else 6)
|
||
head, wd, cy = 70, 34, 35
|
||
if side == 'L':
|
||
L = 150
|
||
jag = [(L + rng.uniform(-10, 10), cy - wd / 2 + wd * k / 6) for k in range(7)]
|
||
pts = [(0, cy - wd / 2)] + jag + [(0, cy + wd / 2)]
|
||
else:
|
||
L = 150
|
||
hx = L - head * 0.9
|
||
jag = [(rng.uniform(-10, 10), cy + wd / 2 - wd * k / 6) for k in range(7)]
|
||
pts = jag + [(hx, cy - wd / 2), (hx, 0), (L, cy), (hx, head), (hx, cy + wd / 2)]
|
||
a = Art(L, head, f'arrowhalf|{side}')
|
||
a.poly(pts, CORAL, rough=0.6)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_sticker(text, color, r=80, size=34, fg=WHITE):
|
||
a = Art(2 * r, 2 * r, f'sticker|{text}|{color}|{r}')
|
||
a.circle(r, r, r, color)
|
||
a.circle(r, r, r - 10, shade(color, 1.1), shade=False, rough=0.5)
|
||
a.text(r, r, text, ('fredoka', size, 'SemiBold'), fg, spacing=2)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_qmark():
|
||
return spr_sticker('?', MUSTARD, r=62, size=96)
|
||
|
||
|
||
@cache
|
||
def spr_star(color=MUSTARD, r=40):
|
||
a = Art(2 * r, 2 * r, f'star|{color}|{r}')
|
||
a.poly(star_pts(r, r, r, r * 0.42), color, rough=0.4)
|
||
return a.build(pad=18)
|
||
|
||
|
||
@cache
|
||
def spr_gear(r, color):
|
||
a = Art(2 * r, 2 * r, f'gear|{r}|{color}')
|
||
a.poly(gear_pts(r, r, r * 0.78, r, 10), color, rough=0.4)
|
||
a.circle(r, r, r * 0.28, shade(color, 0.7), rough=0.3)
|
||
return a.build(pad=16)
|
||
|
||
|
||
@cache
|
||
def spr_cloud(r=34):
|
||
a = Art(3 * r, 2 * r, f'cloud|{r}')
|
||
a.circle(r * 0.9, r * 1.15, r * 0.8, WHITE, shade=False, rough=0.6)
|
||
a.circle(r * 1.6, r * 0.95, r * 0.95, WHITE, shade=False, rough=0.6)
|
||
a.circle(r * 2.2, r * 1.25, r * 0.7, WHITE, shade=False, rough=0.6)
|
||
return a.build(pad=16)
|
||
|
||
|
||
@cache
|
||
def spr_house(roof, w=360, h=330):
|
||
a = Art(w, h, f'house|{roof}|{w}')
|
||
a.rrect(w * 0.68, 22, 44, 96, 4, shade(roof, 0.7))
|
||
a.rrect(32, 128, w - 64, h - 128, 8, PAPER)
|
||
a.poly([(0, 142), (w / 2, 8), (w, 142)], roof)
|
||
a.rrect(w / 2 - 34, h - 112, 68, 112, 10, shade(roof, 0.75))
|
||
a.circle(w / 2 + 20, h - 56, 5, MUSTARD, shade=False, rough=0.2)
|
||
a.rrect(64, 168, 64, 58, 6, SKY, rough=0.5)
|
||
a.rrect(w - 128, 168, 64, 58, 6, SKY, rough=0.5)
|
||
return a.build()
|
||
|
||
|
||
HOUSE_CHIMNEY = (360 * 0.68 + 22 - 180, 22 - 165) # offset from house centre
|
||
|
||
|
||
@cache
|
||
def spr_ink(text, font, color):
|
||
tw, th = measure(text, font)
|
||
a = Art(tw + 12, th + 12, f'ink|{text}|{font}|{color}')
|
||
a.text((tw + 12) / 2, (th + 12) / 2, text, font, color, shade=True)
|
||
return a.build()
|
||
|
||
|
||
def wrap_balanced(s, font, maxw):
|
||
n = wrap(s, font, maxw).count(chr(10))
|
||
if n == 0:
|
||
return s
|
||
lo, hi = 200.0, float(maxw)
|
||
while hi - lo > 6:
|
||
mid = (lo + hi) / 2
|
||
if wrap(s, font, mid).count(chr(10)) <= n:
|
||
hi = mid
|
||
else:
|
||
lo = mid
|
||
return wrap(s, font, hi)
|
||
|
||
|
||
# ================================================================ captions / tags
|
||
CAP_FONT = ('hand', 52)
|
||
|
||
|
||
@cache
|
||
def spr_caption(text):
|
||
s = wrap_balanced(text, CAP_FONT, 1560)
|
||
tw, th = measure(s, CAP_FONT, 4)
|
||
w, h = max(tw + 90, 520), th + 38
|
||
a = Art(w, h, 'cap|' + text)
|
||
a.rrect(0, 0, w, h, 6, PAPER, rough=1.6)
|
||
a.text(w / 2, h / 2 + 2, s, CAP_FONT, INK, spacing=4)
|
||
a.poly(rot_rect_pts(26, 2, 96, 32, -10), (248, 232, 168), rough=0.5, opacity=0.78, tex=0.02, shade=False)
|
||
a.poly(rot_rect_pts(w - 26, 2, 96, 32, 9), (248, 232, 168), rough=0.5, opacity=0.78, tex=0.02, shade=False)
|
||
return a.build()
|
||
|
||
|
||
def draw_caption(c, t0, t1, text, idx):
|
||
if c.t < t0 or c.t > t1 + 0.35:
|
||
return
|
||
spr = spr_caption(text)
|
||
p = prog(c.t, t0, 0.45)
|
||
q = prog(c.t, t1, 0.32)
|
||
y = 1046 - (spr.h - 60) / 2
|
||
y += (1 - ease_back(p, 1.2)) * 200 + ease_in(q) * 220
|
||
rot = (0.8 if idx % 2 else -0.8)
|
||
c.put(spr, 960, y, rot, 1, None, 1, 1.2)
|
||
|
||
|
||
@cache
|
||
def spr_tag(text, color):
|
||
tw, th = measure(text, ('fredoka', 36, 'SemiBold'))
|
||
w, h = tw + 100, 70
|
||
a = Art(w, h, f'tag|{text}|{color}')
|
||
a.poly([(0, 0), (w - 34, 0), (w, h / 2), (w - 34, h), (0, h)], color, rough=1.0)
|
||
a.circle(w - 40, h / 2, 9, shade(color, 0.6), shade=False, rough=0.3)
|
||
a.text(28, h / 2, text, ('fredoka', 36, 'SemiBold'), WHITE, h='l')
|
||
return a.build()
|
||
|
||
|
||
def draw_tag(c, text, color):
|
||
spr = spr_tag(text, color)
|
||
p = prog(c.t, 0.15, 0.7)
|
||
x = lerp(-spr.w / 2 - 20, spr.w / 2 - 20, ease_back(p, 1.1))
|
||
c.put(spr, x, 82, -1.5, 1, None, 1, 1.4)
|
||
|
||
|
||
# ================================================================ shared bits
|
||
def confetti(c, seed, t0, n=16, box=(230, 220, 1690, 760), cols=(CORAL, MUSTARD, TEAL, BLUE, PLUM, ORANGE)):
|
||
rng = random.Random(seed)
|
||
k = 0
|
||
while k < n:
|
||
x, y = rng.uniform(70, 1850), rng.uniform(150, 880)
|
||
kind, col, size, rot = rng.randrange(4), rng.choice(cols), rng.randrange(26, 56), rng.uniform(-40, 40)
|
||
if box[0] < x < box[2] and box[1] < y < box[3]:
|
||
continue
|
||
bob = 5 * math.sin(c.t * 1.7 + k)
|
||
c.pop(spr_confetti(kind, col, size), x, y + bob, t0 + k * 0.09, rot=rot, elev=0.6)
|
||
k += 1
|
||
|
||
|
||
def crane_fly(c, spr, t0, t1, p0, p1, ctrl, s=1.0, flip=False):
|
||
if c.t < t0:
|
||
return
|
||
p = ease_io(prog(c.t, t0, t1 - t0))
|
||
x = (1 - p) ** 2 * p0[0] + 2 * (1 - p) * p * ctrl[0] + p * p * p1[0]
|
||
y = (1 - p) ** 2 * p0[1] + 2 * (1 - p) * p * ctrl[1] + p * p * p1[1]
|
||
dx = 2 * (1 - p) * (ctrl[0] - p0[0]) + 2 * p * (p1[0] - ctrl[0])
|
||
dy = 2 * (1 - p) * (ctrl[1] - p0[1]) + 2 * p * (p1[1] - ctrl[1])
|
||
rot = -math.degrees(math.atan2(dy, abs(dx) + 1e-6)) * 0.35 if p < 1 else 0
|
||
flap = 1 + 0.08 * math.sin(c.t * 10) if p < 1 else 1 + 0.03 * math.sin(c.t * 3)
|
||
y += 10 * math.sin(c.t * 2.1)
|
||
c.put(spr, x, y, rot, s, s * flap, 1, 4.5)
|
||
|
||
|
||
TITLE_COLS = [CORAL, MUSTARD, TEAL, ORANGE, PLUM, CORAL, BRAND, BRAND]
|
||
|
||
|
||
def title_tiles(c, y, t0, pops=None):
|
||
for i, ch in enumerate('MobileGL'):
|
||
tt = t0 + 0.16 * i
|
||
if c.t < tt:
|
||
continue
|
||
p = prog(c.t, tt, 0.65)
|
||
yy = y - 560 * (1 - ease_back(p, 1.6)) + 3 * math.sin(c.t * 2 + i)
|
||
rot = (3.5 if i % 2 else -3.5) + (1 - ease_out(p)) * (25 if i % 2 else -25)
|
||
c.put(spr_tile(ch, TITLE_COLS[i]), 960 - 172 * 3.5 + 172 * i, yy, rot, 1, None, 1,
|
||
1 + 3 * (1 - ease_out(p)))
|
||
|
||
|
||
# ================================================================ scene 1: title
|
||
def s_title(c):
|
||
confetti(c, 11, 0.4)
|
||
title_tiles(c, 400, 0.3)
|
||
c.unfold(spr_label('a desktop OpenGL implementation', ('fredoka', 50, 'Medium'), WHITE, INK,
|
||
padx=40), 960, 612, 2.4, rot=-1)
|
||
crane_fly(c, spr_crane(CORAL), 1.2, 4.4, (-260, 560), (1650, 190), (760, -40))
|
||
|
||
|
||
# ================================================================ scene 2: the problem
|
||
def s_problem(c):
|
||
c.pop(spr_appwin(), 560, 460, 0.4, rot=-2)
|
||
c.pop(spr_bubble('glDrawElements(…)'), 700, 178, 2.0, rot=2)
|
||
c.pop(spr_bubble('glUniform4f(…)'), 360, 205, 3.0, rot=-3)
|
||
c.pop(spr_phone(BG_PEACH), 1320, 480, 6.3, rot=3)
|
||
c.pop(spr_tablet(BG_MINT), 1665, 510, 6.8, rot=-2)
|
||
c.pop(spr_bubble('OpenGL ES', ('fredoka', 42, 'SemiBold'), TEAL, WHITE), 1360, 175, 8.0, rot=-2)
|
||
c.pop(spr_bubble('Vulkan', ('fredoka', 42, 'SemiBold'), PLUM, WHITE, 'br'), 1680, 270, 8.6, rot=2)
|
||
gap = 6 * math.sin(max(0, c.t - 12.6) * 2.4) if c.t > 12.6 else 0
|
||
c.pop(spr_arrow_half('L'), 938 - gap, 470, 12.0, rot=2)
|
||
c.pop(spr_arrow_half('R'), 1122 + gap, 470, 12.2, rot=-3)
|
||
c.pop(spr_qmark(), 1030, 345, 13.0, rot=8)
|
||
|
||
|
||
# ================================================================ scene 3: meet MobileGL
|
||
def s_meet(c):
|
||
c.pop(spr_slab('Your app', 760, CORAL), 960, 190, 0.5)
|
||
c.pop(spr_slab('GPU', 760, DARK), 960, 790, 1.0)
|
||
c.pop(spr_slab('OpenGL ES driver', 420, TEAL, size=40), 742, 625, 1.4)
|
||
c.pop(spr_slab('Vulkan driver', 420, PLUM, size=40), 1178, 625, 1.6)
|
||
c.slide(spr_slab('MobileGL', 900, BRAND, h=130, size=66), (2500, 405), (960, 405), 3.6, d=1.0,
|
||
rot0=-8, rot=0, elev=1.2)
|
||
if c.t > 4.6:
|
||
cr = spr_crane(MUSTARD)
|
||
c.pop(cr, 640, 395, 4.6, s=0.45, rot=-6, elev=1.5)
|
||
for i, (x, y, t0) in enumerate([(960, 300, 5.2), (742, 522, 5.5), (1178, 522, 5.7),
|
||
(742, 710, 5.9), (1178, 710, 6.1)]):
|
||
c.pop(spr_arrow(70, INK, 16, 36), x, y, t0, rot=-90, elev=0.5)
|
||
c.pop(spr_sticker('OpenGL\n4.6 Core', MUSTARD, r=92, size=32, fg=INK), 1480, 200, 6.4, rot=12)
|
||
c.pop(spr_sticker('no app\nchanges', TEAL, r=82, size=30), 450, 405, 17.3, rot=-10)
|
||
|
||
|
||
# ================================================================ scene 4: why it works
|
||
KNOBS = [(110, 150, 'blend'), (250, 150, 'depth'), (390, 150, 'cull'), (530, 150, 'stencil')]
|
||
TOGGLES = [(110, 330, 'texture 0'), (250, 330, 'program'), (390, 330, 'VAO'), (530, 330, 'viewport')]
|
||
BOARD_STATES = [ # (time, knob angles, toggles on)
|
||
(0.0, (30, -40, 90, 0), (1, 0, 1, 1)),
|
||
(13.4, (-60, -40, 90, 0), (1, 0, 1, 1)),
|
||
(14.4, (-60, 50, 90, 0), (1, 1, 1, 1)),
|
||
(15.4, (-60, 50, 90, 0), (0, 1, 1, 1)),
|
||
(19.6, (100, 50, 90, 0), (0, 1, 1, 1)),
|
||
(21.6, (100, 50, -20, 120), (1, 1, 0, 1)),
|
||
]
|
||
|
||
|
||
@cache
|
||
def spr_board():
|
||
a = Art(640, 440, 'board')
|
||
a.rrect(0, 0, 640, 440, 24, NAVY)
|
||
a.rrect(16, 16, 608, 408, 16, shade(NAVY, 1.18), shade=False, rough=0.6)
|
||
for x, y, lab in KNOBS:
|
||
a.circle(x, y, 48, shade(NAVY, 0.8), shade=False, rough=0.5)
|
||
a.text(x, y + 72, lab, ('hand', 30), WHITE)
|
||
for x, y, lab in TOGGLES:
|
||
a.rrect(x - 20, y - 46, 40, 92, 20, shade(NAVY, 0.7), shade=False, rough=0.4)
|
||
a.text(x, y + 72, lab, ('hand', 28), WHITE)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_knob():
|
||
a = Art(80, 80, 'knob')
|
||
a.circle(40, 40, 38, MUSTARD)
|
||
a.rrect(35, 6, 10, 34, 5, DARK, shade=False, rough=0.3)
|
||
return a.build(pad=18)
|
||
|
||
|
||
@cache
|
||
def spr_lever(on):
|
||
a = Art(40, 40, f'lever{on}')
|
||
a.circle(20, 20, 19, CORAL if on else WHITE)
|
||
return a.build(pad=16)
|
||
|
||
|
||
@cache
|
||
def spr_recipe(i):
|
||
items = [['shaders: A + B', 'blend: off', 'depth: less', 'vertex format #2'],
|
||
['shaders: A + B', 'blend: on', 'depth: always', 'vertex format #2'],
|
||
['shaders: C + B', 'blend: on', 'cull: off', 'vertex format #5']][i]
|
||
a = Art(360, 250, f'recipe{i}')
|
||
a.rrect(0, 0, 360, 250, 10, WHITE)
|
||
a.rrect(0, 0, 360, 58, 10, [TEAL, PLUM, ORANGE][i], shade=False)
|
||
a.text(180, 29, f'Draw recipe #{i + 1}', ('fredoka', 30, 'SemiBold'), WHITE)
|
||
a.text(30, 76, '\n'.join('• ' + T(s) for s in items), ('hand', 34), INK, h='l', v='t',
|
||
align='left', spacing=2)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_ingredient(kind):
|
||
a = Art(250, 280, f'ingr|{kind}')
|
||
a.rrect(0, 0, 250, 280, 16, WHITE)
|
||
cx, cy = 125, 115
|
||
if kind == 'buffers':
|
||
for k, col in enumerate([shade(BLUE, 0.8), BLUE, shade(BLUE, 1.25)]):
|
||
a.rrect(45 + k * 8, 60 + k * 34, 150, 52, 8, col)
|
||
a.text(cx + 12, 160, '01101', ('mono', 26), WHITE)
|
||
elif kind == 'textures':
|
||
for i in range(4):
|
||
for j in range(4):
|
||
if (i + j) % 2 == 0:
|
||
a.rrect(53 + i * 36, 43 + j * 36, 36, 36, 2, CORAL, rough=0.4, shade=False)
|
||
else:
|
||
a.rrect(53 + i * 36, 43 + j * 36, 36, 36, 2, MUSTARD, rough=0.4, shade=False)
|
||
elif kind == 'shaders':
|
||
a.rrect(50, 44, 150, 150, 8, PAPER)
|
||
a.rrect(42, 36, 166, 22, 10, shade(PAPER, 0.85))
|
||
a.rrect(42, 182, 166, 22, 10, shade(PAPER, 0.85))
|
||
a.text(66, 72, 'void main()\n{\n color =\n …;\n}', ('mono', 18), INK, h='l', v='t',
|
||
align='left', spacing=3)
|
||
else:
|
||
a.circle(85, cy, 46, NAVY)
|
||
a.circle(85, cy, 32, MUSTARD)
|
||
a.rrect(80, cy - 30, 10, 28, 4, DARK, shade=False)
|
||
a.rrect(155, cy - 52, 40, 104, 20, NAVY)
|
||
a.circle(175, cy - 28, 17, CORAL)
|
||
a.text(cx, 238, kind, ('fredoka', 38, 'SemiBold'), INK)
|
||
return a.build()
|
||
|
||
|
||
def board_state(t):
|
||
st = BOARD_STATES[0]
|
||
prev = st
|
||
for s in BOARD_STATES:
|
||
if t >= s[0]:
|
||
prev, st = st, s
|
||
p = ease_back(prog(t, st[0], 0.45), 2.0) if st is not BOARD_STATES[0] else 1
|
||
knobs = [lerp(a, b, p) for a, b in zip(prev[1], st[1])]
|
||
return knobs, st[2]
|
||
|
||
|
||
def s_why(c):
|
||
t = c.t
|
||
out1 = 11.0
|
||
c.pop(spr_label('OpenGL', ('fredoka', 46, 'SemiBold'), BRAND, WHITE), 560, 250, 1.0, rot=-3, out=out1)
|
||
c.pop(spr_label('OpenGL ES', ('fredoka', 46, 'SemiBold'), TEAL, WHITE), 960, 235, 1.6, rot=2, out=out1 + .1)
|
||
c.pop(spr_label('Vulkan', ('fredoka', 46, 'SemiBold'), PLUM, WHITE), 1360, 250, 2.2, rot=-2, out=out1 + .2)
|
||
for i, kind in enumerate(['buffers', 'textures', 'shaders', 'state']):
|
||
c.pop(spr_ingredient(kind), 480 + 320 * i, 560, 6.3 + 0.45 * i, rot=(-3, 2, -2, 3)[i],
|
||
out=out1 + 0.1 * i)
|
||
if t < 11.3:
|
||
return
|
||
BX, BY = 470, 520
|
||
RX, RY = 1450, 540
|
||
c.pop(spr_label('OpenGL: one big switchboard', ('fredoka', 38, 'SemiBold'), WHITE, INK),
|
||
BX, 215, 11.6, rot=-1.5)
|
||
c.pop(spr_label('Explicit recipes', ('fredoka', 38, 'SemiBold'), WHITE, INK), RX, 215, 17.6, rot=1.5)
|
||
if c.pop(spr_board(), BX, BY, 11.4, rot=-1):
|
||
knobs, togg = board_state(t)
|
||
bp = prog(t, 11.4, 0.55)
|
||
sc = 0.2 + 0.8 * ease_back(bp)
|
||
for (x, y, _), ang in zip(KNOBS, knobs):
|
||
c.put(spr_knob(), BX + (x - 320) * sc, BY + (y - 220) * sc, -ang - 1, sc, None, 1, 0.5)
|
||
for (x, y, _), on in zip(TOGGLES, togg):
|
||
c.put(spr_lever(on), BX + (x - 320) * sc, BY + (y - 220 + (-24 if on else 24)) * sc, 0, sc,
|
||
None, 1, 0.5)
|
||
snaps = [18.3, 20.3, 22.3]
|
||
c.pop(spr_slab('MobileGL', 300, BRAND, h=80, size=44), 960, 330, 17.6, rot=-3)
|
||
for i, ts in enumerate(snaps):
|
||
if t >= ts:
|
||
fp = prog(t, ts, 0.35)
|
||
if fp < 1:
|
||
c.put(spr_star(WHITE, 90), BX, BY, fp * 40, 0.4 + 1.6 * fp, None, 1 - fp, 0, shadow=False)
|
||
dst = (RX + (-30, 20, -10)[i], RY + 70 - 70 * i)
|
||
c.slide(spr_recipe(i), (BX, BY), dst, ts + 0.15, d=1.1, rot=(-4, 3, -1)[i], rot0=20,
|
||
s=1, s0=0.3, arc=220, elev=1 + i * 0.3)
|
||
|
||
|
||
# ================================================================ scene 5: frontend
|
||
NB = (1010, 470)
|
||
TABS = [('Textures', CORAL), ('Uniforms', MUSTARD), ('Blend', TEAL), ('Vertex arrays', PLUM)]
|
||
SLIPS = [(2.0, 'glBindTexture(GL_TEXTURE_2D, 7)', 0), (3.6, 'glUniform4f(tint, 1, .5, 0, 1)', 1),
|
||
(5.2, 'glEnable(GL_BLEND)', 2), (6.8, 'glBindVertexArray(3)', 3),
|
||
(8.4, 'glDrawElements(GL_TRIANGLES, …)', None)]
|
||
|
||
|
||
@cache
|
||
def spr_notebook():
|
||
a = Art(640, 450, 'notebook')
|
||
a.rrect(0, 0, 640, 450, 18, BRAND)
|
||
a.rrect(22, 18, 292, 414, 8, PAPER, rough=0.8)
|
||
a.rrect(326, 18, 292, 414, 8, PAPER, rough=0.8)
|
||
ls = []
|
||
for y in range(110, 420, 44):
|
||
ls += [[(42, y), (296, y)], [(346, y), (598, y)]]
|
||
a.lines(ls, 2, (170, 200, 228), caps=False)
|
||
a.lines([[(320, 26), (320, 424)]], 5, shade(BRAND, 0.7), caps=False)
|
||
a.text(168, 60, 'GL state', ('hand', 38), shade(BRAND, 1.0))
|
||
a.text(472, 60, 'draws', ('hand', 38), shade(BRAND, 1.0))
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_nbtab(text, color):
|
||
a = Art(230, 62, f'nbtab|{text}')
|
||
a.rrect(0, 0, 230, 62, 14, color)
|
||
a.text(142, 31, text, ('fredoka', 26, 'SemiBold'), WHITE)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_slip(text):
|
||
return spr_label(text, ('mono', 30), WHITE, INK, padx=22, pady=14, r=4)
|
||
|
||
|
||
@cache
|
||
def spr_stamp(text):
|
||
font = ('fredoka', 44, 'Bold')
|
||
tw, th = measure(text, font)
|
||
w, h = tw + 56, th + 44
|
||
a = Art(w, h, 'stamp|' + text)
|
||
rp = rrect_pts(0, 0, w, h, 10)
|
||
a.lines([rp + [rp[0]]], 7, RED, tex=0.25, opacity=0.9)
|
||
a.text(w / 2, h / 2, text, font, RED, tex=0.25, opacity=0.9)
|
||
return a.build()
|
||
|
||
|
||
def s_frontend(c):
|
||
t = c.t
|
||
APP = (190, 470)
|
||
c.pop(spr_appwin(), APP[0], APP[1], 0.3, s=0.42, rot=-3)
|
||
nb_on = t >= 0.6
|
||
lifts = [0] * 4
|
||
for ts, txt, tab in SLIPS:
|
||
if tab is not None:
|
||
lifts[tab] = 26 * bump(t, ts + 1.1, 1.3)
|
||
if nb_on:
|
||
for i, (name, col) in enumerate(TABS):
|
||
c.pop(spr_nbtab(name, col), NB[0] + 388 + lifts[i], NB[1] - 150 + 92 * i, 0.9 + 0.12 * i,
|
||
elev=0.6 + lifts[i] / 20)
|
||
c.pop(spr_notebook(), NB[0], NB[1], 0.6, rot=0)
|
||
rows = [NB[1] - 225 + y for y in (132, 220, 308, 396)]
|
||
for k, (ts, txt, tab) in enumerate(SLIPS):
|
||
if tab is not None:
|
||
dst = (NB[0] - 152, rows[tab] - 10)
|
||
else:
|
||
dst = (NB[0] + 152, NB[1] - 225 + 132)
|
||
c.slide(spr_slip(txt), APP, dst, ts, d=1.1, s=0.45, s0=0.8, rot=(-2 if k % 2 else 2), rot0=-10,
|
||
arc=160, elev=0.3)
|
||
# the bad call
|
||
bad = spr_slip('glBindTexture(GL_TEXTURE_2D, 999)')
|
||
if t >= 12.8:
|
||
if t < 15.6:
|
||
c.slide(bad, APP, (520, 770), 12.8, d=1.0, s=0.62, s0=0.6, rot=4, rot0=-8, arc=160)
|
||
sx, sy = 520, 770
|
||
else:
|
||
p = prog(t, 15.6, 1.2)
|
||
sx, sy = lerp(520, 60, ease_in(p)), 770 + 420 * ease_in(p)
|
||
c.put(bad, sx, sy, 4 + 40 * p, 0.62, None, 1 - p * 0.3, 2)
|
||
if t >= 14.1:
|
||
sp = prog(t, 14.1, 0.25)
|
||
c.put(spr_stamp('GL_INVALID_OPERATION'), sx + 20, sy + 4, -8, 0.55 * (1.8 - 0.8 * ease_out(sp)),
|
||
None, min(1, sp * 3) * (1 - prog(t, 16.4, 0.4)), 0, shadow=False)
|
||
if t >= 18.5:
|
||
bob = 6 * math.sin(t * 2)
|
||
c.pop(spr_gpu(170), 1700, 790 + bob, 18.7, rot=4)
|
||
for k in range(3):
|
||
ph = (t * 0.7 + k / 3) % 1
|
||
c.put(spr_ink('z', ('fredoka', 40 + 12 * k, 'Bold'), BRAND),
|
||
1780 + 50 * ph + 10 * k, 700 - 110 * ph - 10 * k, 10, 0.6 + 0.6 * ph, None,
|
||
min(1, (1 - ph) * 3), 0.5)
|
||
|
||
|
||
# ================================================================ scene 6: shaders
|
||
GLSL = ['#version 460 core', 'in vec3 pos;', 'uniform mat4 mvp;', '', 'void main() {',
|
||
' gl_Position =', ' mvp * vec4(pos, 1);', '}']
|
||
GLSLES = ['#version 320 es', 'precision highp', ' float;', 'in vec3 pos;', 'uniform …;',
|
||
'void main() {', ' gl_Position = …;', '}']
|
||
|
||
|
||
@cache
|
||
def spr_scroll(title, lines, color=PAPER, tcol=BRAND):
|
||
n = len(lines)
|
||
w, h = 340, 130 + n * 31
|
||
a = Art(w, h, f'scroll|{title}')
|
||
a.rrect(12, 18, w - 24, h - 36, 4, color)
|
||
a.rrect(0, 0, w, 36, 18, shade(color, 0.86))
|
||
a.rrect(0, h - 36, w, 36, 18, shade(color, 0.86))
|
||
a.text(w / 2, 66, title, ('fredoka', 34, 'SemiBold'), tcol)
|
||
a.text(34, 96, '\n'.join(lines), ('mono', 21), INK, h='l', v='t', align='left', spacing=10)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_machine(name, color, w=360, h=260):
|
||
a = Art(w, h + 70, f'machine|{name}|{color}')
|
||
a.poly([(28, 0), (168, 0), (128, 76), (68, 76)], shade(color, 0.8))
|
||
a.rrect(0, 64, w, h, 24, color)
|
||
a.rrect(w - 10, 170, 26, 100, 6, DARK)
|
||
a.rrect(30, h - 10, w - 60, 66, 12, WHITE)
|
||
a.text(w / 2, h + 23, name, ('fredoka', 40, 'SemiBold'), INK)
|
||
return a.build()
|
||
|
||
|
||
def s_shaders(c):
|
||
t = c.t
|
||
MX, MY = 900, 450
|
||
if t < 3.0:
|
||
c.pop(spr_scroll('GLSL', tuple(GLSL)), 300, 440, 0.4, rot=-3)
|
||
else:
|
||
p = prog(t, 3.0, 1.3)
|
||
if p < 1:
|
||
c.put(spr_scroll('GLSL', tuple(GLSL)), lerp(300, MX - 82, ease_io(p)),
|
||
lerp(440, MY - 205, ease_io(p)) - 120 * math.sin(math.pi * p), -3 + 20 * p,
|
||
1 - 0.85 * ease_in(p), None, 1, 1 + 2 * math.sin(math.pi * p))
|
||
busy = 3.6 < t < 6.4
|
||
spin = (t * (220 if busy else 30))
|
||
if c.pop(spr_machine('glslang', MUSTARD), MX, MY, 1.0):
|
||
shake = 2.5 * math.sin(t * 40) if busy else 0
|
||
c.put(spr_gear(46, CORAL), MX - 60 + shake, MY - 30, spin, 1, None, 1, 0.5)
|
||
c.put(spr_gear(34, TEAL), MX + 28 + shake, MY - 50, -spin * 1.35 + 18, 1, None, 1, 0.5)
|
||
crane = spr_crane(BLUE)
|
||
if t >= 5.8:
|
||
if t < 12.6:
|
||
c.slide(crane, (MX + 190, MY + 40), (1250, 420), 5.8, d=0.9, s=0.8, s0=0.2, arc=120, elev=1.5)
|
||
c.pop(spr_label('SPIR-V', ('fredoka', 40, 'SemiBold'), BLUE, WHITE), 1250, 560, 6.4, rot=-2,
|
||
out=12.4)
|
||
else:
|
||
c.slide(crane, (1250, 420), (1530, 240), 12.6, d=1.0, s=0.7, s0=0.8, arc=60, elev=1.5)
|
||
c.slide(crane, (1250, 420), (1380, 680), 12.6, d=1.0, s=0.3, s0=0.8, arc=40, elev=1.0,
|
||
out=13.6, od=0.3)
|
||
if t >= 12.6:
|
||
c.pop(spr_label('Magma (Vulkan): uses it as-is', ('fredoka', 36, 'SemiBold'), PLUM, WHITE), 1560, 380,
|
||
13.4, rot=2)
|
||
if c.pop(spr_machine('SPIRV-Cross', TEAL, w=330, h=200), 1380, 700, 12.4, s=0.62):
|
||
c.put(spr_gear(34, MUSTARD), 1380 - 40, 700 - 34, t * 160, 0.62, None, 1, 0.5)
|
||
c.pop(spr_scroll('GLSL ES', tuple(GLSLES), tcol=TEAL), 1730, 620, 14.6, s=0.78, rot=3)
|
||
c.pop(spr_label('Espryt (OpenGL ES): GLSL ES', ('fredoka', 34, 'SemiBold'), TEAL, WHITE), 1560, 862,
|
||
15.0, rot=-2)
|
||
|
||
|
||
# ================================================================ scene 7: records
|
||
BELT_Y = 560
|
||
BX0, BX1 = 330, 1590
|
||
|
||
|
||
@cache
|
||
def spr_belt():
|
||
a = Art(1320, 64, 'belt')
|
||
a.rrect(0, 0, 1320, 64, 32, DARK)
|
||
a.rrect(14, 12, 1292, 16, 8, shade(DARK, 1.35), shade=False, rough=0.5)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_roller():
|
||
a = Art(40, 40, 'roller')
|
||
a.circle(20, 20, 19, GREY)
|
||
a.lines([[(20, 4), (20, 36)]], 4, DARK, shade=False)
|
||
return a.build(pad=14)
|
||
|
||
|
||
@cache
|
||
def spr_record(text, color):
|
||
tw, th = measure(text, ('hand', 34))
|
||
w, h = tw + 60, 66
|
||
a = Art(w, h, f'record|{text}|{color}')
|
||
a.rrect(0, 0, w, h, 8, WHITE)
|
||
a.rrect(0, 0, 20, h, 6, color, shade=False)
|
||
a.text(20 + (w - 20) / 2, h / 2, text, ('hand', 34), INK)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_box(name, color, w=230, h=170, size=40):
|
||
a = Art(w, h, f'box|{name}|{color}|{w}')
|
||
a.rrect(0, 0, w, h, 20, color)
|
||
a.rrect(16, 16, w - 32, h - 32, 12, shade(color, 1.12), shade=False, rough=0.5)
|
||
a.text(w / 2, h / 2, name, ('fredoka', size, 'SemiBold'), WHITE)
|
||
return a.build()
|
||
|
||
|
||
def ride(c, spr, ts, y=BELT_Y - 62, v=270, extra=None):
|
||
"""Card spawned at ts from the frontend, rides the belt into the backend."""
|
||
if c.t < ts:
|
||
return None
|
||
x = BX0 + v * (c.t - ts)
|
||
if x > BX1:
|
||
p = prog(x, BX1, 110)
|
||
if p >= 1:
|
||
return None
|
||
c.put(spr, x, y - 30 * p, 0, 1 - 0.8 * p, None, 1 - p, 1)
|
||
return None
|
||
pp = prog(c.t, ts, 0.35)
|
||
c.put(spr, x, y - 60 * (1 - ease_back(pp)), 0, 0.6 + 0.4 * ease_out(pp), None, 1, 1)
|
||
return x
|
||
|
||
|
||
def s_records(c):
|
||
t = c.t
|
||
c.pop(spr_belt(), (BX0 + BX1) / 2, BELT_Y, 0.4, elev=0.6)
|
||
if t >= 0.6:
|
||
for k in range(11):
|
||
c.put(spr_roller(), BX0 - 10 + k * 128, BELT_Y, -t * 260 + k * 30, 0.9, None, 1, 0.3)
|
||
c.pop(spr_box('Frontend', BRAND), 190, 520, 0.6, rot=-2)
|
||
c.pop(spr_house(TEAL, 300, 280), 1745, 470, 0.8)
|
||
c.pop(spr_label('Backend', ('fredoka', 36, 'SemiBold'), TEAL, WHITE), 1745, 660, 1.0, rot=2)
|
||
seq = [(1.6, spr_record('texture 7 on unit 0', CORAL)), (2.6, spr_record('blend: on', TEAL)),
|
||
(3.6, spr_record('viewport 1920×1080', MUSTARD)),
|
||
(4.7, spr_label('DRAW', ('fredoka', 44, 'Bold'), ORANGE, WHITE, padx=36)),
|
||
(8.6, spr_record('uniform tint', MUSTARD)),
|
||
(9.7, spr_label('DRAW', ('fredoka', 44, 'Bold'), ORANGE, WHITE, padx=36)),
|
||
(24.0, spr_record('texture 12 on unit 1', CORAL)),
|
||
(25.0, spr_label('DRAW', ('fredoka', 44, 'Bold'), ORANGE, WHITE, padx=36))]
|
||
for ts, spr in seq:
|
||
ride(c, spr, ts)
|
||
c.pop(spr_sticker('only\nchanges', CORAL, r=78, size=30), 760, 300, 9.0, rot=-8, out=17.8)
|
||
# handle tag riding with a new object
|
||
x = ride(c, spr_record('new texture', BLUE), 12.4)
|
||
if x is not None:
|
||
c.put(spr_label('handle {slot 12, gen 3}', ('mono', 24), MUSTARD, INK, padx=14, pady=8, r=6),
|
||
x + 10, BELT_Y + 70, -3, 1, None, 1, 0.8)
|
||
c.pop(spr_sticker('no\nwaiting', BLUE, r=74, size=30), 1210, 300, 13.4, rot=8, out=17.8)
|
||
# dedupe: two identical blocks merge
|
||
blk = spr_record('blend on · depth less', PLUM)
|
||
MP = (700, 360)
|
||
if 18.4 <= t < 21.2:
|
||
p = ease_io(prog(t, 18.5, 1.2))
|
||
if p < 1:
|
||
c.put(blk, lerp(450, MP[0], p), lerp(250, MP[1], p), lerp(-8, 0, p), 1, None, 1, 2)
|
||
c.put(blk, lerp(980, MP[0], p), lerp(440, MP[1], p), lerp(7, 0, p), 1, None, 1, 2)
|
||
else:
|
||
q = ease_io(prog(t, 20.3, 0.9))
|
||
c.put(blk, lerp(MP[0], BX0, q), lerp(MP[1], BELT_Y - 62, q) - 80 * math.sin(math.pi * q), 0, 1,
|
||
None, 1, 2)
|
||
if 19.6 <= t < 20.4:
|
||
sp = prog(t, 19.6, 0.8)
|
||
c.put(spr_star(MUSTARD, 46), MP[0] + 150, MP[1] - 40, sp * 90, 0.4 + 0.9 * math.sin(math.pi * sp),
|
||
None, 1, 1)
|
||
if t >= 18.4:
|
||
c.pop(spr_sticker('sent\nonce', MUSTARD, r=74, size=32, fg=INK), 1060, 280, 19.9, rot=10)
|
||
ride(c, blk, 21.2)
|
||
|
||
|
||
# ================================================================ scene 8: backends
|
||
EH, MH = (560, 470), (1360, 470)
|
||
|
||
|
||
def s_backends(c):
|
||
t = c.t
|
||
c.pop(spr_house(CORAL), EH[0], EH[1], 0.4, rot=-1)
|
||
c.pop(spr_house(TEAL), MH[0], MH[1], 0.7, rot=1)
|
||
c.pop(spr_label('Espryt', ('fredoka', 52, 'SemiBold'), CORAL, WHITE), EH[0], 700, 6.2, rot=-2)
|
||
c.pop(spr_label('speaks OpenGL ES', ('hand', 40), WHITE, INK, pady=8), EH[0], 790, 6.6, rot=1)
|
||
c.pop(spr_label('Magma', ('fredoka', 52, 'SemiBold'), TEAL, WHITE), MH[0], 700, 7.6, rot=2)
|
||
c.pop(spr_label('speaks Vulkan', ('hand', 40), WHITE, INK, pady=8), MH[0], 790, 8.0, rot=-1)
|
||
c.pop(spr_gpu(150), 960, 540, 1.2, rot=-3)
|
||
which = 0 if t < 9.5 or (13 < t < 16.5) else 1
|
||
if t >= 9.0:
|
||
val = ['DirectGLES', 'DirectVulkan'][which]
|
||
k1 = spr_label('MOBILEGL_BACKEND_TYPE=', ('mono', 34), DARK, WHITE, padx=24, pady=14, r=6)
|
||
k2 = spr_label(val, ('mono', 34), [CORAL, TEAL][which], WHITE, padx=20, pady=14, r=6)
|
||
i1, i2 = k1.w - 60, k2.w - 60
|
||
x0 = 960 - (i1 + i2) / 2
|
||
c.pop(k1, x0 + i1 / 2, 175, 9.0)
|
||
c.put(k2, x0 + i1 + i2 / 2 + 8, 175, -2 if which else 2, 1, None, min(1, (t - 9.0) * 4), 1.6)
|
||
# records rain into the active house
|
||
active = (EH, MH)[which] if t >= 9.0 else (EH if t < 4.8 else MH)
|
||
for k in range(40):
|
||
ts = 1.6 + k * 0.55
|
||
if not (ts <= t < ts + 1.0):
|
||
continue
|
||
tgt = (EH, MH)[0 if t < 9.0 and ts < 4.8 else which] if t >= 9.0 or ts < 4.8 else MH
|
||
p = prog(t, ts, 1.0)
|
||
col = (CORAL, TEAL, MUSTARD, PLUM)[k % 4]
|
||
c.put(spr_confetti(2, col, 34), lerp(960, tgt[0], ease_io(p)), lerp(260, tgt[1] + 40, ease_io(p))
|
||
- 120 * math.sin(math.pi * p), p * 200, 1 - 0.4 * p, None, 1 - ease_in(p), 1.5)
|
||
# chimney puffs of the active house
|
||
for k in range(4):
|
||
ph = (t * 0.6 + k / 4) % 1
|
||
hx, hy = active[0] + HOUSE_CHIMNEY[0], active[1] + HOUSE_CHIMNEY[1]
|
||
if t > 2:
|
||
c.put(spr_cloud(26), hx + 40 * ph, hy - 20 - 140 * ph, 0, 0.5 + 0.8 * ph, None,
|
||
min(1, ph * 4) * (1 - ph), 0.4)
|
||
if t >= 12.0:
|
||
c.pop(spr_sticker('fully\nseparate', MUSTARD, r=80, size=30, fg=INK), 960, 330, 12.2, rot=-6)
|
||
|
||
|
||
# ================================================================ scene 9: anywhere
|
||
PANEL_X = [285, 735, 1185, 1635]
|
||
PANEL_Y = 470
|
||
|
||
|
||
@cache
|
||
def spr_fb(name, color, w=140):
|
||
a = Art(w, 58, f'fb|{name}|{color}|{w}')
|
||
a.rrect(0, 0, w, 58, 14, color)
|
||
a.text(w / 2, 29, name, ('fredoka', 26, 'SemiBold'), WHITE)
|
||
return a.build()
|
||
|
||
|
||
def _wavy(x, y0, y1, amp=7, ph=0.0):
|
||
return [(x + amp * math.sin(y / 22 + ph), y) for y in range(int(y0), int(y1) + 1, 6)]
|
||
|
||
|
||
def _dashed(x, y, w, h, r=16, dash=14):
|
||
pts = rrect_pts(x, y, w, h, r)
|
||
pts.append(pts[0])
|
||
out, acc, cur = [], 0.0, []
|
||
for (x0, y0), (x1, y1) in zip(pts, pts[1:]):
|
||
L = math.hypot(x1 - x0, y1 - y0)
|
||
n = max(1, int(L / 3))
|
||
for k in range(n):
|
||
px, py = x0 + (x1 - x0) * k / n, y0 + (y1 - y0) * k / n
|
||
on = int(acc / dash) % 2 == 0
|
||
if on:
|
||
cur.append((px, py))
|
||
elif cur:
|
||
out.append(cur)
|
||
cur = []
|
||
acc += L / n
|
||
if cur:
|
||
out.append(cur)
|
||
return [s for s in out if len(s) > 1]
|
||
|
||
|
||
@cache
|
||
def spr_panel(i):
|
||
titles = ['One thread', 'Two threads', 'Two processes', 'Two machines']
|
||
notes = ['monolith', 'inproc', 'spawn + shared memory', 'tcp://host:port']
|
||
a = Art(400, 440, f'panel{i}')
|
||
a.rrect(0, 0, 400, 440, 18, WHITE)
|
||
a.text(200, 44, titles[i], ('fredoka', 38, 'SemiBold'), INK)
|
||
a.text(200, 410, notes[i], ('mono', 22) if i == 3 else ('hand', 30), GREY)
|
||
if i == 0:
|
||
a.lines([_wavy(200, 92, 370)], 6, CORAL, shade=True)
|
||
elif i == 1:
|
||
a.lines(_dashed(26, 90, 348, 290), 3, GREY, caps=False)
|
||
a.lines([_wavy(115, 100, 368), _wavy(285, 100, 368, ph=2)], 6, CORAL, shade=True)
|
||
elif i == 2:
|
||
a.lines(_dashed(18, 90, 168, 200) + _dashed(214, 90, 168, 200), 3, GREY, caps=False)
|
||
return a.build()
|
||
|
||
|
||
@cache
|
||
def spr_ring():
|
||
a = Art(130, 130, 'ring')
|
||
a.circle(65, 65, 62, DARK)
|
||
for k in range(8):
|
||
ang = 2 * math.pi * k / 8
|
||
a.poly(rot_rect_pts(65 + 46 * math.cos(ang), 65 + 46 * math.sin(ang), 22, 18, math.degrees(ang)),
|
||
(CORAL, MUSTARD, TEAL, BLUE)[k % 4], rough=0.3, shade=False)
|
||
a.circle(65, 65, 30, WHITE)
|
||
return a.build(pad=16)
|
||
|
||
|
||
def s_anywhere(c):
|
||
t = c.t
|
||
pts = [1.0, 6.0, 11.0, 16.0]
|
||
F, B = spr_fb('frontend', BRAND), spr_fb('backend', TEAL)
|
||
for i, (x, ts) in enumerate(zip(PANEL_X, pts)):
|
||
if not c.pop(spr_panel(i), x, PANEL_Y, ts, rot=(-2, 1.5, -1, 2)[i]):
|
||
continue
|
||
ox, oy = x - 200, PANEL_Y - 220
|
||
sc = 0.2 + 0.8 * ease_back(prog(t, ts, 0.55))
|
||
if sc < 0.97:
|
||
continue
|
||
if i == 0:
|
||
c.put(F, ox + 200, oy + 180, 0, 1, None, 1, 0.5)
|
||
c.put(B, ox + 200, oy + 270, 0, 1, None, 1, 0.5)
|
||
elif i == 1:
|
||
c.put(spr_fb('frontend', BRAND, 130), ox + 115, oy + 200, 0, 1, None, 1, 0.5)
|
||
c.put(spr_fb('backend', TEAL, 130), ox + 285, oy + 290, 0, 1, None, 1, 0.5)
|
||
ph = (t * 0.9) % 1
|
||
c.put(spr_confetti(2, MUSTARD, 28), lerp(ox + 130, ox + 270, ph), lerp(oy + 225, oy + 265, ph)
|
||
- 60 * math.sin(math.pi * ph), ph * 180, 1, None, min(1, (1 - ph) * 6, ph * 6), 1.2)
|
||
elif i == 2:
|
||
c.put(spr_fb('frontend', BRAND, 130), ox + 102, oy + 160, 0, 1, None, 1, 0.5)
|
||
c.put(spr_fb('backend', TEAL, 130), ox + 298, oy + 230, 0, 1, None, 1, 0.5)
|
||
c.put(spr_ring(), ox + 200, oy + 330, -t * 70, 0.85, None, 1, 0.8)
|
||
else:
|
||
c.put(spr_laptop(SKY), ox + 105, oy + 170, 0, 0.5, None, 1, 0.6)
|
||
c.put(spr_phone(BG_PEACH, 230, 440), ox + 320, oy + 250, 4, 0.36, None, 1, 0.6)
|
||
c.put(spr_label('client', ('hand', 30), BRAND, WHITE, padx=12, pady=4), ox + 105, oy + 250, -3,
|
||
1, None, 1, 0.5)
|
||
c.put(spr_label('server', ('hand', 30), TEAL, WHITE, padx=12, pady=4), ox + 320, oy + 352, 3,
|
||
1, None, 1, 0.5)
|
||
for k in range(2):
|
||
ph = (t * 0.55 + k * 0.5) % 1
|
||
px = lerp(ox + 140, ox + 300, ph)
|
||
py = lerp(oy + 150, oy + 220, ph) - 70 * math.sin(math.pi * ph)
|
||
c.put(spr_plane(MUSTARD if k else CORAL), px, py, -20 + 40 * ph, 0.45, None,
|
||
min(1, (1 - ph) * 6, ph * 6), 2.5)
|
||
c.pop(spr_label('client = frontend, inside your app', ('fredoka', 34, 'SemiBold'), BRAND, WHITE),
|
||
560, 790, 21.8, rot=-2)
|
||
c.pop(spr_label('server = backend, next to the GPU', ('fredoka', 34, 'SemiBold'), TEAL, WHITE),
|
||
1360, 800, 22.6, rot=2)
|
||
|
||
|
||
# ================================================================ scene 10: perks
|
||
@cache
|
||
def spr_perk(i):
|
||
title, body, color = [
|
||
('Fast split', 'Two threads or processes can even beat one: both sides work at once.', MUSTARD),
|
||
('Run-ahead', 'Keep sending. Wait only for the answers you truly need.', CORAL),
|
||
('Zero-copy', 'Big buffers and window images are shared, not copied.', BLUE),
|
||
('Safe failure', "Server gone? Your app sees 'device lost', not a crash.", TEAL)][i]
|
||
a = Art(410, 500, f'perk{i}')
|
||
a.rrect(0, 0, 410, 500, 18, WHITE)
|
||
a.rrect(0, 0, 410, 86, 18, color, shade=False)
|
||
a.text(205, 44, title, ('fredoka', 42, 'SemiBold'), WHITE if i != 0 else INK)
|
||
cx, cy = 205, 190
|
||
if i == 0:
|
||
a.lines([_wavy(cx - 50, cy - 70, cy + 70), _wavy(cx + 50, cy - 70, cy + 70, ph=2)], 7, CORAL, shade=True)
|
||
a.rrect(cx - 50 - 60, cy - 22, 120, 44, 12, BRAND)
|
||
a.rrect(cx + 50 - 60, cy + 4, 120, 44, 12, TEAL)
|
||
elif i == 1:
|
||
for k in range(3):
|
||
px, py, sc = cx - 110 + 85 * k, cy + 40 - 32 * k, 0.55
|
||
a.poly([(px, py), (px + 170 * sc, py + 30 * sc), (px + 52 * sc, py + 30 * sc)], shade(CORAL, 1.25), rough=0.3)
|
||
a.poly([(px + 52 * sc, py + 30 * sc), (px + 170 * sc, py + 30 * sc), (px + 22 * sc, py + 70 * sc)], CORAL, rough=0.3)
|
||
elif i == 2:
|
||
a.rrect(cx - 150, cy - 70, 170, 130, 12, DARK)
|
||
a.rrect(cx - 20, cy - 50, 170, 130, 12, DARK)
|
||
a.rrect(cx - 60, cy - 34, 120, 90, 6, MUSTARD)
|
||
else:
|
||
a.poly([(cx - 70, cy - 80), (cx + 70, cy - 80), (cx + 70, cy), (cx, cy + 86), (cx - 70, cy)], TEAL)
|
||
a.lines([[(cx - 32, cy - 6), (cx - 6, cy + 22), (cx + 38, cy - 34)]], 14, WHITE, shade=True)
|
||
a.text(205, 300, wrap(body, ('hand', 36), 350), ('hand', 36), INK, v='t', spacing=4)
|
||
return a.build()
|
||
|
||
|
||
def s_perks(c):
|
||
for i, ts in enumerate([0.8, 6.8, 12.8, 18.8]):
|
||
bob = 4 * math.sin(c.t * 1.5 + i)
|
||
lift = 1 if c.t < ts + 0.6 or c.t > ts + 5.8 else 1.8
|
||
c.pop(spr_perk(i), 255 + 470 * i, 480 + bob, ts, rot=(-2, 1.5, -1.5, 2)[i], elev=lift)
|
||
|
||
|
||
# ================================================================ scene 11: platforms
|
||
@cache
|
||
def spr_platform(i):
|
||
name, body, color = [
|
||
('Android', 'Its first home: Minecraft: Java Edition via launchers like FoldCraftLauncher.', TEAL),
|
||
('Linux', 'EGL, GLX, Wayland and glvnd. Even a whole KDE Plasma desktop.', MUSTARD),
|
||
('Windows', 'A drop-in opengl32.dll that speaks WGL.', BLUE),
|
||
('macOS', 'CGL and NSOpenGL, on Vulkan via MoltenVK.', PLUM)][i]
|
||
a = Art(400, 560, f'plat{i}')
|
||
a.rrect(0, 0, 400, 560, 18, WHITE)
|
||
a.rrect(0, 0, 400, 230, 18, color, shade=False)
|
||
cx, cy = 200, 118
|
||
if i == 0:
|
||
a.rrect(cx - 58, cy - 96, 116, 196, 18, DARK)
|
||
a.rrect(cx - 48, cy - 76, 96, 150, 6, BG_SKY, shade=False)
|
||
a.poly([(cx, cy - 40), (cx + 34, cy - 22), (cx, cy - 4), (cx - 34, cy - 22)], GREEN, rough=0.3)
|
||
a.poly([(cx - 34, cy - 22), (cx, cy - 4), (cx, cy + 36), (cx - 34, cy + 18)], DIRT, rough=0.3)
|
||
a.poly([(cx + 34, cy - 22), (cx, cy - 4), (cx, cy + 36), (cx + 34, cy + 18)], shade(DIRT, 0.75), rough=0.3)
|
||
elif i == 1:
|
||
a.rrect(cx - 120, cy - 78, 240, 160, 14, DARK)
|
||
a.text(cx - 92, cy - 30, '$ glmark2\n> _', ('mono', 26), (140, 220, 140), h='l', v='t', align='left', spacing=10)
|
||
elif i == 2:
|
||
a.rrect(cx - 120, cy - 80, 240, 164, 12, WHITE)
|
||
a.rrect(cx - 120, cy - 80, 240, 38, 12, DARK, shade=False)
|
||
a.text(cx, cy + 22, 'opengl32.dll', ('mono', 26), INK)
|
||
else:
|
||
a.rrect(cx - 110, cy - 82, 220, 144, 12, DARK)
|
||
a.rrect(cx - 98, cy - 70, 196, 120, 6, SKY, shade=False)
|
||
a.poly([(cx - 140, cy + 66), (cx + 140, cy + 66), (cx + 124, cy + 88), (cx - 124, cy + 88)], GREY)
|
||
a.text(200, 280, name, ('fredoka', 50, 'SemiBold'), INK)
|
||
a.text(200, 330, wrap(body, ('hand', 34), 340), ('hand', 34), INK, v='t', spacing=4)
|
||
return a.build()
|
||
|
||
|
||
def s_platforms(c):
|
||
t = c.t
|
||
for i, ts in enumerate([0.8, 6.8, 7.3, 7.8]):
|
||
hl = bump(t, 12.6, 6.6) if i == 1 else 0
|
||
bob = 4 * math.sin(t * 1.5 + i)
|
||
c.pop(spr_platform(i), 285 + 450 * i, 480 + bob - 30 * hl, ts, rot=(-2, 1.5, -1.5, 2)[i] * (1 - hl),
|
||
s=1 + 0.06 * hl, elev=1 + 2 * hl)
|
||
if 12.9 <= t:
|
||
c.pop(spr_sticker('KDE Plasma\non a tablet', CORAL, r=96, size=30), 950, 180, 13.2, rot=10,
|
||
out=19.2)
|
||
|
||
|
||
# ================================================================ scene 12: outro
|
||
def s_outro(c):
|
||
confetti(c, 23, 0.6, n=18, box=(300, 200, 1620, 900))
|
||
title_tiles(c, 330, 0.3)
|
||
c.unfold(spr_label('free & open source · LGPL-3.0', ('fredoka', 46, 'Medium'), WHITE, INK, padx=40),
|
||
960, 530, 2.4, rot=-1)
|
||
c.unfold(spr_label('github.com/MobileGL-Dev/MobileGL', ('mono', 44), BRAND, WHITE, padx=36, pady=18),
|
||
960, 645, 3.1, rot=1)
|
||
crane_fly(c, spr_crane(MUSTARD), 1.0, 4.6, (2200, 600), (300, 170), (1100, -60))
|
||
c.pop(spr_label('Come fold with us!', ('fredoka', 64, 'SemiBold'), CORAL, WHITE, padx=50, pady=22),
|
||
960, 820, 20.9, rot=-2)
|
||
|
||
|
||
# ================================================================ timeline
|
||
SCENES = [
|
||
dict(fn=s_title, dur=16, bg=BG_CREAM, tag=None, caps=[
|
||
(4.6, 9.6, 'Run desktop OpenGL apps where there is no desktop OpenGL driver.'),
|
||
(9.9, 15.0, "Let's unfold how it works. No deep internals, promise!")]),
|
||
dict(fn=s_problem, dur=20, bg=BG_SKY, tag=('The problem', CORAL), caps=[
|
||
(0.6, 5.8, 'Lots of great software speaks desktop OpenGL: games, creative tools, whole desktops.'),
|
||
(6.1, 11.6, 'But phones and tablets usually ship only OpenGL ES or Vulkan drivers.'),
|
||
(11.9, 19.0, "Same GPU, different language: the app's calls have nobody to answer them.")]),
|
||
dict(fn=s_meet, dur=22, bg=BG_BUTTER, tag=('Meet MobileGL', BRAND), caps=[
|
||
(0.5, 5.8, 'Meet MobileGL: a complete desktop OpenGL library that sits between your app and the driver.'),
|
||
(6.1, 11.4, 'Your app sees OpenGL 4.6 Core, with just a few corners still being finished.'),
|
||
(11.7, 16.8, 'Underneath, MobileGL speaks whatever the device understands: OpenGL ES or Vulkan.'),
|
||
(17.1, 21.2, "Your app doesn't change. It simply loads MobileGL as its OpenGL library.")]),
|
||
dict(fn=s_why, dur=28, bg=BG_MINT, tag=('Why it works', TEAL), caps=[
|
||
(0.5, 5.8, 'Why can this work at all? Every modern GPU API is built from the same ingredients:'),
|
||
(6.1, 10.9, 'buffers, textures, shaders, and state like blending and depth testing.'),
|
||
(11.4, 17.0, 'OpenGL keeps that state in one big global switchboard you flip one switch at a time.'),
|
||
(17.3, 22.8, 'At each draw, MobileGL snapshots the switches that matter into an explicit recipe…'),
|
||
(23.1, 27.2, '…that a backend can replay on OpenGL ES or Vulkan.')]),
|
||
dict(fn=s_frontend, dur=26, bg=BG_PEACH, tag=('Step 1 · The frontend', CORAL), caps=[
|
||
(0.5, 5.8, 'Step 1: the frontend. Every GL call your app makes lands here first.'),
|
||
(6.1, 11.8, 'It tracks every bind, every uniform, every switch, just like a desktop driver would.'),
|
||
(12.1, 17.8, 'It checks each call against the spec, so mistakes become proper GL errors, not crashes.'),
|
||
(18.1, 25.2, 'Nothing has touched the GPU yet. The frontend simply knows what the world should look like.')]),
|
||
dict(fn=s_shaders, dur=24, bg=BG_LAV, tag=('Shaders get folded', PLUM), caps=[
|
||
(0.5, 5.8, 'Shaders get folded too. Your GLSL is compiled once, by glslang…'),
|
||
(6.1, 11.8, '…into SPIR-V: a compact, portable shader format.'),
|
||
(12.1, 17.8, 'Vulkan takes SPIR-V as-is. For OpenGL ES, SPIRV-Cross unfolds it into GLSL ES.'),
|
||
(18.1, 23.2, 'One shader, folded two ways, and your app never notices.')]),
|
||
dict(fn=s_records, dur=28, bg=BG_CREAM, tag=('Step 2 · Records', ORANGE), caps=[
|
||
(0.5, 5.8, 'Step 2: before each draw, the frontend sends the backend only what changed.'),
|
||
(6.1, 11.8, 'Each change is a small record. The draw itself is a verb that uses them.'),
|
||
(12.1, 17.8, 'New objects get their handles up front, so creating things never waits for a reply.'),
|
||
(18.1, 23.6, 'Identical state blocks are shared instead of resent. Less to carry, less to check.'),
|
||
(23.9, 27.2, "The backend never peeks into the frontend's memory. Records are all it gets.")]),
|
||
dict(fn=s_backends, dur=24, bg=BG_SKY, tag=('Step 3 · Backends', TEAL), caps=[
|
||
(0.5, 5.8, 'Step 3: a backend turns records into real driver calls.'),
|
||
(6.1, 11.8, 'Espryt speaks OpenGL ES. Magma speaks Vulkan. You pick one at startup.'),
|
||
(12.1, 17.8, 'Each backend is fully independent, so one can improve without disturbing the other.'),
|
||
(18.1, 23.2, 'Same app, same frontend. Choose whatever suits the device best.')]),
|
||
dict(fn=s_anywhere, dur=28, bg=BG_BUTTER, tag=('Records can travel', BRAND), caps=[
|
||
(0.5, 5.6, "Now the fun part: records are just data, so they don't care where the backend lives."),
|
||
(5.9, 10.6, 'Run everything on one thread, or give frontend and backend a thread each…'),
|
||
(10.9, 15.6, '…put them in two processes that share a ring of memory…'),
|
||
(15.9, 21.2, "…or even on two machines: a PC can drive a phone's GPU over the network."),
|
||
(21.5, 27.2, 'The frontend side is the client. The backend side, next to the GPU, is the server.')]),
|
||
dict(fn=s_perks, dur=24, bg=BG_MINT, tag=('Built for the trip', ORANGE), caps=[
|
||
(0.5, 6.3, "Splitting up isn't slow. Under heavy load it can even be faster than one thread."),
|
||
(6.6, 12.4, 'The client runs ahead and only waits when it really needs an answer back.'),
|
||
(12.7, 18.4, 'On the same machine, big buffers and window images are shared, not copied.'),
|
||
(18.7, 23.4, "And if the server goes away, your app sees 'device lost' instead of crashing.")]),
|
||
dict(fn=s_platforms, dur=26, bg=BG_LAV, tag=('Where it runs', PLUM), caps=[
|
||
(0.5, 6.3, 'MobileGL grew up on Android: think Minecraft: Java Edition on a phone.'),
|
||
(6.6, 12.4, 'It also runs on Linux, slips into Windows as opengl32.dll, and works on macOS.'),
|
||
(12.7, 19.0, 'A favorite demo: a whole KDE Plasma desktop from a Linux container, rendered by MobileGL on an Android tablet.'),
|
||
(19.3, 25.2, 'Same library everywhere. Only the window-system glue changes.')]),
|
||
dict(fn=s_outro, dur=24, bg=BG_NAVY, tag=None, caps=[
|
||
(4.2, 9.6, 'MobileGL is free, open source, and still growing.'),
|
||
(9.9, 15.4, 'Build it, pick a backend, and point your favorite OpenGL app at it.'),
|
||
(15.7, 20.6, "Something breaks? Tell us. That's the most useful thing you can do.")]),
|
||
]
|
||
TR = 0.8
|
||
STARTS = []
|
||
_acc = 0.0
|
||
for _s in SCENES:
|
||
STARTS.append(_acc)
|
||
_acc += _s['dur']
|
||
TOTAL = _acc
|
||
NFRAMES = int(TOTAL * FPS)
|
||
|
||
|
||
def render_frame(fi):
|
||
t = fi / FPS
|
||
i = max(k for k in range(len(SCENES)) if STARTS[k] <= t + 1e-9)
|
||
sc = SCENES[i]
|
||
lt = t - STARTS[i]
|
||
frame = background(sc['bg']).copy()
|
||
c = Ctx(frame, lt, fi, sc['dur'])
|
||
sc['fn'](c)
|
||
if sc['tag']:
|
||
draw_tag(c, *sc['tag'])
|
||
for k, (a, b, txt) in enumerate(sc['caps']):
|
||
draw_caption(c, a, b, txt, k)
|
||
if i + 1 < len(SCENES) and lt > sc['dur'] - TR:
|
||
draw_wipe(frame, SCENES[i + 1]['bg'], prog(lt, sc['dur'] - TR, TR))
|
||
out = frame.convert('RGB')
|
||
fade = min(prog(t, 0, 0.7), 1 - prog(t, TOTAL - 1.6, 1.5))
|
||
if fade < 1:
|
||
out = Image.blend(Image.new('RGB', out.size, (0, 0, 0)), out, fade)
|
||
return out
|
||
|
||
|
||
def _worker(fi):
|
||
return render_frame(fi).tobytes()
|
||
|
||
|
||
def write_meta(path):
|
||
caps, ev = [], []
|
||
for st, sc in zip(STARTS, SCENES):
|
||
for a, b, txt in sc['caps']:
|
||
caps.append((st + a, st + b, T(txt)))
|
||
ev.append(st + sc['dur'] - TR)
|
||
json.dump(dict(total=TOTAL, starts=STARTS, wipes=ev[:-1],
|
||
pops=[0.3 + 0.16 * i for i in range(8)] + [STARTS[-1] + 0.3 + 0.16 * i for i in range(8)]),
|
||
open(path + '.json', 'w'), indent=1)
|
||
|
||
def ts(x):
|
||
ms = int(round(x * 1000))
|
||
return f'{ms // 3600000:02d}:{ms // 60000 % 60:02d}:{ms // 1000 % 60:02d},{ms % 1000:03d}'
|
||
with open(path + '.srt', 'w', encoding='utf-8') as f:
|
||
for n, (a, b, txt) in enumerate(caps, 1):
|
||
f.write(f'{n}\n{ts(a)} --> {ts(b)}\n{txt}\n\n')
|
||
|
||
|
||
OUT = os.path.join(HERE, 'out' if LANG == 'en' else 'out_' + LANG)
|
||
SUFFIX = '' if LANG == 'en' else '_' + LANG
|
||
|
||
if __name__ == '__main__':
|
||
os.makedirs(OUT, exist_ok=True)
|
||
if sys.argv[1] == 'stills':
|
||
for s in sys.argv[2:]:
|
||
t = float(s)
|
||
render_frame(int(round(t * FPS))).save(os.path.join(OUT, f'still_{t:06.1f}.png'))
|
||
print('still', t)
|
||
elif sys.argv[1] == 'meta':
|
||
write_meta(os.path.join(OUT, 'mobilegl_intro' + SUFFIX))
|
||
print('total', TOTAL)
|
||
elif sys.argv[1] == 'render':
|
||
write_meta(os.path.join(OUT, 'mobilegl_intro' + SUFFIX))
|
||
ff = sys.argv[2]
|
||
dst = os.path.join(OUT, 'video_noaudio' + SUFFIX + '.mp4')
|
||
p = subprocess.Popen([ff, '-y', '-loglevel', 'error', '-f', 'rawvideo', '-pix_fmt', 'rgb24',
|
||
'-s', f'{W}x{H}', '-r', str(FPS), '-i', '-', '-r', '24', '-c:v', 'libx264',
|
||
'-preset', 'slow', '-crf', '18', '-tune', 'animation', '-pix_fmt', 'yuv420p', dst],
|
||
stdin=subprocess.PIPE)
|
||
t0 = time.time()
|
||
with Pool(max(2, os.cpu_count() - 2)) as pool:
|
||
for k, buf in enumerate(pool.imap(_worker, range(NFRAMES), chunksize=6)):
|
||
p.stdin.write(buf)
|
||
if k % 240 == 0:
|
||
print(f'{k}/{NFRAMES} {time.time() - t0:.0f}s', flush=True)
|
||
p.stdin.close()
|
||
p.wait()
|
||
print('done', time.time() - t0)
|