LihonT 2 Luna pre-release
Avatar shapes (circle, flower, rounded square, hexagon, heart, triangle): the own shape everywhere and, through the members service, the shapes other members published after proving the account with the code in the bio. Own accent colour and AMOLED, compact bottom bar, Material 3 profile and plus button, save menu (quality, audio only, photo posts without the watermark), fixed comment cards, "Luna (pre-release)" in the mod screen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
LihonT 2 «Luna beta» promo video: 1080x1920, 30 fps, about 28 seconds, with a synthesised soundtrack.
|
||||
LihonT 2 «Luna pre-release» promo video: 1080x1920, 30 fps, about 37 seconds, with a synthesised soundtrack.
|
||||
|
||||
The screens are real screenshots of the mod on a phone, taken to work/promo/shots/ (the status bar and the
|
||||
gesture strip are cut off). Frames are drawn with Pillow in worker processes and piped to ffmpeg.
|
||||
@@ -38,10 +38,12 @@ SCENES = [
|
||||
("intro", 0.0, 3.6),
|
||||
("system", 3.6, 8.0),
|
||||
("comments", 8.0, 12.4),
|
||||
("badges", 12.4, 16.4),
|
||||
("settings", 16.4, 21.2),
|
||||
("icons", 21.2, 24.4),
|
||||
("outro", 24.4, 28.4),
|
||||
("shapes", 12.4, 17.2),
|
||||
("save", 17.2, 21.8),
|
||||
("badges", 21.8, 25.8),
|
||||
("settings", 25.8, 30.6),
|
||||
("icons", 30.6, 33.8),
|
||||
("outro", 33.8, 37.8),
|
||||
]
|
||||
DURATION = SCENES[-1][2]
|
||||
FRAMES = int(round(DURATION * FPS))
|
||||
@@ -60,10 +62,10 @@ NAV_CUT = 66 # gesture strip
|
||||
|
||||
# Faces of people other than the author are blurred in the comment shots: (left, top, right, bottom) in shot pixels.
|
||||
BLUR = {
|
||||
"comments_beta5.png": [
|
||||
(121, 998, 261, 1138), (290, 1002, 605, 1047),
|
||||
(238, 1366, 311, 1439), (341, 1360, 669, 1401),
|
||||
(137, 1667, 248, 1776), (290, 1658, 620, 1700),
|
||||
"comments.png": [
|
||||
(56, 1312, 184, 1436), (206, 1304, 580, 1360),
|
||||
(206, 1628, 300, 1718), (306, 1618, 654, 1676),
|
||||
(54, 1892, 184, 2016), (206, 1884, 554, 1940),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -235,12 +237,13 @@ def init_assets() -> None:
|
||||
ASSETS["motes"] = [(rnd.random() * W, rnd.random() * H, 0.4 + rnd.random() * 0.9, rnd.random() * 6.28) for _ in range(16)]
|
||||
ASSETS["logo"] = Image.open(LOGO).convert("RGBA")
|
||||
shots = {
|
||||
"comments": "comments_beta5.png", "menu": "menu.png", "native": "settings_native.png",
|
||||
"comments": "comments.png", "menu": "menu.png", "native": "settings_native.png",
|
||||
"mod1": "mod_settings1.png", "mod2": "mod_settings2.png", "profile": "profile.png",
|
||||
"shapes": "shapes.png", "save": "save.png",
|
||||
}
|
||||
cache = {key: load_shot(name) for key, name in shots.items()}
|
||||
ASSETS["shots"] = cache
|
||||
ASSETS["phones"] = {key: build_phone(cache[key]) for key in ("comments", "menu", "native")}
|
||||
ASSETS["phones"] = {key: build_phone(cache[key]) for key in ("comments", "menu", "native", "shapes", "save")}
|
||||
ASSETS["mod_screens"] = [screen_for(cache["mod1"]), screen_for(cache["mod2"])]
|
||||
ASSETS["mod_shell"] = phone_shell(*ASSETS["mod_screens"][0].size)
|
||||
ASSETS["icons"] = {slug: rounded(Image.open(ROOT / "src" / "icons" / (slug + ".png")).convert("RGB").resize((320, 320), Image.Resampling.LANCZOS), 72)
|
||||
@@ -293,7 +296,7 @@ def scene_intro(layer: Image.Image, lt: float) -> None:
|
||||
a = ease_out_cubic((lt - 0.7) / 0.5)
|
||||
draw_text(layer, (W / 2, 1180 + (1 - a) * 50), "LihonT 2", 150, ON + (int(255 * a),))
|
||||
b = ease_out_cubic((lt - 1.0) / 0.5)
|
||||
draw_text(layer, (W / 2, 1320 + (1 - b) * 40), "Luna · бета", 64, PRIMARY + (int(255 * b),))
|
||||
draw_text(layer, (W / 2, 1320 + (1 - b) * 40), "Luna · пре-релиз", 64, PRIMARY + (int(255 * b),))
|
||||
c = ease_out_cubic((lt - 1.4) / 0.5)
|
||||
draw_text(layer, (W / 2, 1430 + (1 - c) * 30), "Мод для TikTok в стиле Material 3", 44, MUTED + (int(255 * c),), bold=False)
|
||||
|
||||
@@ -319,6 +322,69 @@ def scene_comments(layer: Image.Image, lt: float) -> None:
|
||||
chip(layer, 850, 1780, "Поле-пилюля", pop(lt, 1.7))
|
||||
|
||||
|
||||
def shape_silhouette(kind: str, size: int, color: tuple) -> Image.Image:
|
||||
"""The six avatar outlines of the mod (the geometry of lihont.two.avatar.Shapes), filled with one colour."""
|
||||
scale = 4
|
||||
s = size * scale
|
||||
layer = Image.new("RGBA", (s, s), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(layer)
|
||||
fill = color + (255,) if len(color) == 3 else color
|
||||
if kind == "flower":
|
||||
r, pr = s * 0.31, s * 0.19
|
||||
d.ellipse([s / 2 - r, s / 2 - r, s / 2 + r, s / 2 + r], fill=fill)
|
||||
for k in range(6):
|
||||
a = k * math.pi / 3 - math.pi / 2
|
||||
px, py = s / 2 + math.cos(a) * s * 0.31, s / 2 + math.sin(a) * s * 0.31
|
||||
d.ellipse([px - pr, py - pr, px + pr, py + pr], fill=fill)
|
||||
elif kind == "squircle":
|
||||
d.rounded_rectangle([0, 0, s - 1, s - 1], radius=int(s * 0.34), fill=fill)
|
||||
elif kind == "hexagon":
|
||||
points = [(s * (0.5 + 0.5 * math.cos(math.pi / 6 + k * math.pi / 3 - math.pi / 2 + math.pi / 6)),
|
||||
s * (0.5 + 0.5 * math.sin(math.pi / 6 + k * math.pi / 3 - math.pi / 2 + math.pi / 6)))
|
||||
for k in range(6)]
|
||||
d.polygon(points, fill=fill)
|
||||
elif kind == "heart":
|
||||
def bezier(p0, p1, p2, p3, n=24):
|
||||
return [((1 - t) ** 3 * p0[0] + 3 * (1 - t) ** 2 * t * p1[0] + 3 * (1 - t) * t * t * p2[0] + t ** 3 * p3[0],
|
||||
(1 - t) ** 3 * p0[1] + 3 * (1 - t) ** 2 * t * p1[1] + 3 * (1 - t) * t * t * p2[1] + t ** 3 * p3[1])
|
||||
for t in [i / n for i in range(n + 1)]]
|
||||
q = lambda x, y: (s * x, s * y) # noqa: E731
|
||||
points = (bezier(q(0.5, 0.94), q(0.10, 0.62), q(0.02, 0.40), q(0.10, 0.24))
|
||||
+ bezier(q(0.10, 0.24), q(0.18, 0.08), q(0.40, 0.06), q(0.5, 0.27))
|
||||
+ bezier(q(0.5, 0.27), q(0.60, 0.06), q(0.82, 0.08), q(0.90, 0.24))
|
||||
+ bezier(q(0.90, 0.24), q(0.98, 0.40), q(0.90, 0.62), q(0.5, 0.94)))
|
||||
d.polygon(points, fill=fill)
|
||||
elif kind == "triangle":
|
||||
d.polygon([(s * 0.5, s * 0.05), (s * 0.96, s * 0.86), (s * 0.04, s * 0.86)], fill=fill)
|
||||
else:
|
||||
d.ellipse([0, 0, s - 1, s - 1], fill=fill)
|
||||
return layer.resize((size, size), Image.Resampling.LANCZOS)
|
||||
|
||||
|
||||
SHAPE_KINDS = ["circle", "flower", "squircle", "hexagon", "heart", "triangle"]
|
||||
|
||||
|
||||
def scene_shapes(layer: Image.Image, lt: float) -> None:
|
||||
headline(layer, lt, "Форма аватарок", "Её видят все, у кого есть мод")
|
||||
rise = ease_out_cubic((lt - 0.15) / 0.9)
|
||||
place(layer, ASSETS["phones"]["shapes"], W / 2, 1010 + (1 - rise) * 900 + math.sin(lt * 2) * 6, 0.72, -3)
|
||||
for n, kind in enumerate(SHAPE_KINDS):
|
||||
k = pop(lt, 1.0 + n * 0.18)
|
||||
place(layer, shape_silhouette(kind, 130, PRIMARY), 170 + n * 148, 1650, k)
|
||||
chip(layer, 215, 640, "Круг, как в TikTok", pop(lt, 2.0), size=36)
|
||||
chip(layer, 870, 760, "Без мода — круг", pop(lt, 2.4), size=36)
|
||||
|
||||
|
||||
def scene_save(layer: Image.Image, lt: float) -> None:
|
||||
headline(layer, lt, "Скачать как надо", "Качество, звук, все фото")
|
||||
rise = ease_out_cubic((lt - 0.15) / 0.9)
|
||||
tilt = (1 - ease_out_cubic(lt / 1.2)) * 8
|
||||
place(layer, ASSETS["phones"]["save"], W / 2, 1200 + (1 - rise) * 900 + math.sin(lt * 2) * 6, 1.0, tilt)
|
||||
chip(layer, 190, 690, "Без водяного знака", pop(lt, 0.9), size=36)
|
||||
chip(layer, 890, 1010, "Только звук", pop(lt, 1.3), size=36)
|
||||
chip(layer, 880, 1500, "Фото карусели", pop(lt, 1.7), size=36)
|
||||
|
||||
|
||||
def scene_badges(layer: Image.Image, lt: float) -> None:
|
||||
headline(layer, lt, "Значки у ников", "TikTok You, MargyT и нишевый мод")
|
||||
profile = ASSETS["shots"]["profile"]
|
||||
@@ -351,7 +417,7 @@ def scene_settings(layer: Image.Image, lt: float) -> None:
|
||||
chip(layer, 170, 760, "Без водяного знака", pop(lt, 0.9) * gone, size=36)
|
||||
chip(layer, 905, 960, "Убрать рекламу", pop(lt, 1.2) * gone, size=36)
|
||||
chip(layer, 185, 1260, "Убрать эфиры", pop(lt, 1.5) * gone, size=36)
|
||||
chip(layer, 890, 1500, "Скрыть «Магазин»", pop(lt, 2.4), size=36)
|
||||
chip(layer, 890, 1500, "Форма и цвет аватарок", pop(lt, 2.4), size=36)
|
||||
|
||||
|
||||
def scene_icons(layer: Image.Image, lt: float) -> None:
|
||||
@@ -370,7 +436,7 @@ def scene_outro(layer: Image.Image, lt: float) -> None:
|
||||
place(layer, ASSETS["blobs"][0], W / 2, 640, 1.3 * k)
|
||||
place(layer, ASSETS["logo"], W / 2, 640, 0.40 * k * pulse, math.sin(lt * 2) * 3)
|
||||
a = ease_out_cubic((lt - 0.4) / 0.5)
|
||||
draw_text(layer, (W / 2, 960 + (1 - a) * 40), "Скачай бету", 110, ON + (int(255 * a),))
|
||||
draw_text(layer, (W / 2, 960 + (1 - a) * 40), "Скачай пре-релиз", 100, ON + (int(255 * a),))
|
||||
b = ease_out_cubic((lt - 0.7) / 0.5)
|
||||
draw_text(layer, (W / 2, 1075 + (1 - b) * 30), "LihonT 2 · Luna", 56, PRIMARY + (int(255 * b),))
|
||||
chip(layer, W / 2, 1290, "git.lihont.space", pop(lt, 1.0), size=54, fill=PRIMARY, fg=BG)
|
||||
@@ -380,8 +446,9 @@ def scene_outro(layer: Image.Image, lt: float) -> None:
|
||||
|
||||
|
||||
SCENE_DRAW = {
|
||||
"intro": scene_intro, "system": scene_system, "comments": scene_comments, "badges": scene_badges,
|
||||
"settings": scene_settings, "icons": scene_icons, "outro": scene_outro,
|
||||
"intro": scene_intro, "system": scene_system, "comments": scene_comments, "shapes": scene_shapes,
|
||||
"save": scene_save, "badges": scene_badges, "settings": scene_settings, "icons": scene_icons,
|
||||
"outro": scene_outro,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user