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:
111
scripts/deploy_members.py
Normal file
111
scripts/deploy_members.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Swap the LihonT members service (badges and avatar shapes) on the Helsinki server and roll it back.
|
||||
|
||||
py scripts/deploy_members.py --dry-run compare the live server.py with the local one, change nothing
|
||||
py scripts/deploy_members.py back up, upload, restart lihont-members, roll back on failure
|
||||
py scripts/deploy_members.py --rollback restore the newest backup (or --stamp <timestamp>)
|
||||
|
||||
Only /opt/lihont-members/server.py is replaced; its database is never touched (new tables are created by the
|
||||
service itself when it starts, so an older server.py keeps working on the same file). The live file is copied to
|
||||
server.py.bak-<timestamp> first, the new one is compiled on the server before it goes live, and the service is
|
||||
asked for its health after the restart; a failed check puts the backup back.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
HOST = "hels"
|
||||
FOLDER = "/opt/lihont-members"
|
||||
LIVE = FOLDER + "/server.py"
|
||||
SERVICE = "lihont-members"
|
||||
HEALTH = "http://127.0.0.1:18746/members/v1/health"
|
||||
SOURCE = Path(__file__).resolve().parents[1] / "services" / "members" / "server.py"
|
||||
SSH = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", HOST]
|
||||
|
||||
|
||||
def remote(command: str, data: bytes | None = None) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(SSH + [command], input=data, capture_output=True, timeout=120)
|
||||
|
||||
|
||||
def text(result: subprocess.CompletedProcess) -> str:
|
||||
# ssh prints a post-quantum key exchange warning on stderr; it is not an error.
|
||||
lines = [line for line in result.stderr.decode("utf-8", "replace").splitlines() if line and not line.startswith("**")]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def digest_of_live() -> str:
|
||||
result = remote("sha256sum %s" % shlex.quote(LIVE))
|
||||
return result.stdout.decode().split()[0] if result.returncode == 0 and result.stdout else "?"
|
||||
|
||||
|
||||
def healthy() -> bool:
|
||||
return remote("curl -sf %s" % shlex.quote(HEALTH)).returncode == 0
|
||||
|
||||
|
||||
def deploy(dry_run: bool) -> int:
|
||||
data = SOURCE.read_bytes()
|
||||
local = hashlib.sha256(data).hexdigest()
|
||||
live = digest_of_live()
|
||||
print("local %s\nlive %s" % (local, live))
|
||||
if dry_run:
|
||||
print("Dry run: nothing was changed." if local != live else "Already deployed.")
|
||||
return 0
|
||||
if local == live:
|
||||
print("The server already runs this file.")
|
||||
return 0
|
||||
stamp = time.strftime("%Y%m%d-%H%M%S")
|
||||
backup = "%s.bak-%s" % (LIVE, stamp)
|
||||
result = remote("set -e; cat > /tmp/members-server.new && python3 -m py_compile /tmp/members-server.new "
|
||||
"&& cp -p %s %s && install -m 644 /tmp/members-server.new %s && rm -f /tmp/members-server.new"
|
||||
% (shlex.quote(LIVE), shlex.quote(backup), shlex.quote(LIVE)), data)
|
||||
if result.returncode:
|
||||
print("Upload failed, the live file is untouched:\n" + text(result))
|
||||
return 1
|
||||
print("backup: %s" % backup)
|
||||
remote("systemctl restart %s" % SERVICE)
|
||||
time.sleep(2)
|
||||
if healthy():
|
||||
print("The service answers; deployed.")
|
||||
return 0
|
||||
print("The service did not answer: putting the backup back.")
|
||||
remote("cp -p %s %s && systemctl restart %s" % (shlex.quote(backup), shlex.quote(LIVE), SERVICE))
|
||||
time.sleep(2)
|
||||
print("Rolled back; the service answers: %s" % healthy())
|
||||
return 1
|
||||
|
||||
|
||||
def rollback(stamp: str | None) -> int:
|
||||
if stamp:
|
||||
backup = "%s.bak-%s" % (LIVE, stamp)
|
||||
else:
|
||||
listing = remote("ls -1 %s.bak-2* 2>/dev/null | tail -1" % shlex.quote(LIVE)).stdout.decode().strip()
|
||||
if not listing:
|
||||
print("No dated backup found.")
|
||||
return 1
|
||||
backup = listing
|
||||
result = remote("cp -p %s %s && systemctl restart %s" % (shlex.quote(backup), shlex.quote(LIVE), SERVICE))
|
||||
if result.returncode:
|
||||
print("Rollback failed:\n" + text(result))
|
||||
return 1
|
||||
time.sleep(2)
|
||||
print("Restored %s; the service answers: %s" % (backup, healthy()))
|
||||
return 0 if healthy() else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--rollback", action="store_true")
|
||||
parser.add_argument("--stamp")
|
||||
args = parser.parse_args()
|
||||
return rollback(args.stamp) if args.rollback else deploy(args.dry_run)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
164
scripts/make_lihont_cards.py
Normal file
164
scripts/make_lihont_cards.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
News cards for the Telegram post about LihonT 2 «Luna pre-release»: 1080x1350 PNGs in work/promo/cards/.
|
||||
Uses the same screenshots and drawing helpers as the promo video (make_lihont_promo.py).
|
||||
|
||||
usage (from the repository root): py -B scripts/make_lihont_cards.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import make_lihont_promo as P # noqa: E402
|
||||
|
||||
CW, CH = 1080, 1350
|
||||
OUT_DIR = P.ROOT / "work" / "promo" / "cards"
|
||||
|
||||
|
||||
def backdrop(seed: int) -> Image.Image:
|
||||
img = Image.new("RGBA", (CW, CH), P.BG + (255,))
|
||||
rnd = random.Random(seed)
|
||||
spots = [(0.25, 0.2), (0.8, 0.55), (0.3, 0.9)]
|
||||
for n, blob_img in enumerate(P.ASSETS["blobs"]):
|
||||
fx, fy = spots[n]
|
||||
img.alpha_composite(blob_img, (int(CW * fx - blob_img.width / 2), int(CH * fy - blob_img.height / 2)))
|
||||
for _ in range(12):
|
||||
img.alpha_composite(P.ASSETS["flower"], (int(rnd.random() * CW), int(rnd.random() * CH)))
|
||||
return img
|
||||
|
||||
|
||||
def head(layer: Image.Image, title: str, sub: str, y: int = 150) -> None:
|
||||
P.draw_text(layer, (CW / 2, y), title, 84, P.ON + (255,))
|
||||
P.draw_text(layer, (CW / 2, y + 96), sub, 42, P.PRIMARY + (255,), bold=False)
|
||||
|
||||
|
||||
def footer(layer: Image.Image) -> None:
|
||||
P.draw_text(layer, (CW / 2, CH - 44), "LihonT 2 · Luna pre-release · @lihont", 32, P.MUTED + (255,), bold=False)
|
||||
|
||||
|
||||
def card_hero() -> Image.Image:
|
||||
img = backdrop(1)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
P.place(layer, P.ASSETS["blobs"][1], CW / 2, 470, 1.4)
|
||||
P.place(layer, P.ASSETS["logo"], CW / 2, 470, 0.5)
|
||||
P.draw_text(layer, (CW / 2, 830), "LihonT 2", 150, P.ON + (255,))
|
||||
P.draw_text(layer, (CW / 2, 960), "Luna · пре-релиз", 64, P.PRIMARY + (255,))
|
||||
P.draw_text(layer, (CW / 2, 1040), "Мод для TikTok в стиле Material 3", 44, P.MUTED + (255,), bold=False)
|
||||
P.chip(layer, CW / 2, 1170, "Скачать: git.lihont.space", 1.0, size=48, fill=P.PRIMARY, fg=P.BG)
|
||||
P.chip(layer, CW / 2, 1270, "Telegram · @lihont", 1.0, size=40)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def card_system() -> Image.Image:
|
||||
img = backdrop(2)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
head(layer, "Всё в стиле Material 3", "Меню, настройки, нижняя панель")
|
||||
P.place(layer, P.ASSETS["phones"]["menu"], 300, 930, 0.82, 6)
|
||||
P.place(layer, P.ASSETS["phones"]["native"], 790, 990, 0.82, -5)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def card_comments() -> Image.Image:
|
||||
img = backdrop(3)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
head(layer, "Новые комментарии", "Карточки, ручка шторки, поле-пилюля")
|
||||
P.place(layer, P.ASSETS["phones"]["comments"], CW / 2, 930, 0.95)
|
||||
P.chip(layer, 190, 560, "Карточки", 1.0, size=38)
|
||||
P.chip(layer, 890, 470, "Ручка шторки", 1.0, size=38)
|
||||
P.chip(layer, 860, 1230, "Поле-пилюля", 1.0, size=38)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def card_shapes() -> Image.Image:
|
||||
img = backdrop(7)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
head(layer, "Форма аватарок", "Её видят все, у кого есть мод")
|
||||
P.place(layer, P.ASSETS["phones"]["shapes"], CW / 2, 800, 0.62, -3)
|
||||
for n, kind in enumerate(P.SHAPE_KINDS):
|
||||
P.place(layer, P.shape_silhouette(kind, 120, P.PRIMARY), 190 + n * 140, 1230, 1.0)
|
||||
P.chip(layer, 200, 560, "Круг, как в TikTok", 1.0, size=34)
|
||||
P.chip(layer, 880, 640, "Без мода — круг", 1.0, size=34)
|
||||
footer(layer)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def card_save() -> Image.Image:
|
||||
img = backdrop(8)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
head(layer, "Скачать как надо", "Качество, звук, все фото")
|
||||
P.place(layer, P.ASSETS["phones"]["save"], CW / 2, 930, 0.95)
|
||||
P.chip(layer, 190, 520, "Без водяного знака", 1.0, size=36)
|
||||
P.chip(layer, 890, 760, "Только звук", 1.0, size=36)
|
||||
P.chip(layer, 880, 1230, "Фото карусели", 1.0, size=36)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def card_feed() -> Image.Image:
|
||||
img = backdrop(4)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
head(layer, "Лента без лишнего", "Включай только нужное")
|
||||
first, _second = P.ASSETS["mod_screens"]
|
||||
phone = P.mount(P.ASSETS["mod_shell"], first)
|
||||
P.place(layer, phone, 640, 960, 0.9)
|
||||
for n, text in enumerate(["Без водяного знака", "Убрать рекламу", "Убрать эфиры", "Скрыть «Магазин»"]):
|
||||
P.chip(layer, 230, 520 + n * 190, text, 1.0, size=34)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def card_badges() -> Image.Image:
|
||||
img = backdrop(5)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
head(layer, "Значки у ников", "TikTok You, MargyT и нишевый мод")
|
||||
profile = P.ASSETS["shots"]["profile"]
|
||||
card = P.rounded(profile.crop((0, 0, 1080, 900)).resize((940, 783), Image.Resampling.LANCZOS), 56)
|
||||
P.place(layer, card, CW / 2, 720, 1.0)
|
||||
zoom = P.rounded(profile.crop((20, 166, 700, 270)).resize((880, 135), Image.Resampling.LANCZOS), 40)
|
||||
ring = P.rounded(Image.new("RGBA", (896, 151), P.PRIMARY + (255,)), 48)
|
||||
P.place(layer, ring, CW / 2, 1210)
|
||||
P.place(layer, zoom, CW / 2, 1210)
|
||||
footer(layer)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def card_icons() -> Image.Image:
|
||||
img = backdrop(6)
|
||||
layer = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
|
||||
head(layer, "Иконки приложения", "Выбери свою или добавь свою")
|
||||
spots = [(340, 560), (740, 560), (340, 980), (740, 980)]
|
||||
for (slug, label), (x, y) in zip(P.ICONS, spots):
|
||||
P.place(layer, P.ASSETS["icons"][slug], x, y, 1.0)
|
||||
P.draw_text(layer, (x, y + 205), label, 40, P.ON + (255,))
|
||||
footer(layer)
|
||||
img.alpha_composite(layer)
|
||||
return img
|
||||
|
||||
|
||||
def main() -> None:
|
||||
P.init_assets()
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
cards = [("1_hero", card_hero), ("2_material3", card_system), ("3_comments", card_comments),
|
||||
("4_shapes", card_shapes), ("5_save", card_save), ("6_feed", card_feed),
|
||||
("7_badges", card_badges), ("8_icons", card_icons)]
|
||||
for name, make in cards:
|
||||
path = OUT_DIR / ("card_%s.png" % name)
|
||||
make().convert("RGB").save(path)
|
||||
print(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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