Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
277 lines
9.9 KiB
Python
277 lines
9.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Генератор логотипа PixelLauncher — пиксель-арт на чистом stdlib.
|
|
|
|
Рисует логотип на логическом поле 32x32 и масштабирует x8 -> 256x256 PNG.
|
|
|
|
Запуск:
|
|
python assets/make_logo.py # варианты land/gold/face -> превью
|
|
python assets/make_logo.py land out.png # конкретный вариант в файл
|
|
|
|
Варианты:
|
|
land — пиксельная «P», сверху трава, снизу земля (буква-как-блок);
|
|
gold — золотая «P» на тёмном фоне с травяной полосой;
|
|
face — зелёный блок с тёмной «P» (родственник крипера).
|
|
"""
|
|
import os
|
|
import struct
|
|
import sys
|
|
import zlib
|
|
|
|
S = 32 # логическое поле
|
|
K = 8 # масштаб -> 256
|
|
|
|
# палитра сайта/лаунчера
|
|
DARK = (27, 31, 43) # #1b1f2b фон
|
|
FRAME = (14, 16, 22) # тёмная рамка
|
|
LINE = (51, 58, 77) # #333a4d
|
|
GOLD = (255, 180, 84) # #ffb454
|
|
GOLD_D = (217, 171, 52) # #d9ab34
|
|
GOLD_L = (255, 217, 138)
|
|
GRASS = (124, 179, 66) # rgb(124,179,66)
|
|
GRASS_D = (90, 143, 50)
|
|
GRASS_L = (148, 201, 95)
|
|
DIRT = (125, 88, 54) # #7d5836
|
|
DIRT_D = (107, 74, 47) # #6b4a2f
|
|
INK = (12, 13, 18)
|
|
|
|
# пиксельная «P»: 6 шириной, 8 высотой? — см. LETTER (x: 6 колонок, 8 строк)
|
|
LETTER = [
|
|
"XXXXXX",
|
|
"X..XX.", # снизу-вверх перевернём: рисуем снизу вверх? см. ниже
|
|
]
|
|
|
|
# настоящая «P» 6x8 (X = заливка)
|
|
LETTER = [
|
|
"XXXXXX",
|
|
"X..XX.",
|
|
"X..XX.",
|
|
"XXXXXX",
|
|
"XX....",
|
|
"XX....",
|
|
"XX....",
|
|
"XX....",
|
|
]
|
|
|
|
# большая «P» 10x16 для 32x32 поля
|
|
LETTER_BIG = [
|
|
"XXXXXXXXXX",
|
|
"XX......XX",
|
|
"XX......XX",
|
|
"XX......XX",
|
|
"XX......XX",
|
|
"XX......XX",
|
|
"XXXXXXXXXX",
|
|
"XX........",
|
|
"XX........",
|
|
"XX........",
|
|
"XX........",
|
|
"XX........",
|
|
"XX........",
|
|
"XX........",
|
|
"XX........",
|
|
"XX........",
|
|
]
|
|
|
|
|
|
def canvas():
|
|
return [[None] * S for _ in range(S)]
|
|
|
|
|
|
def in_round_rect(x, y, x0, y0, x1, y1, r):
|
|
"""Пиксель внутри прямоугольника со скруглёнными углами (жёсткие углы)."""
|
|
if not (x0 <= x <= x1 and y0 <= y <= y1):
|
|
return False
|
|
for cx, cy in ((x0 + r, y0 + r), (x1 - r, y0 + r),
|
|
(x0 + r, y1 - r), (x1 - r, y1 - r)):
|
|
if ((x < x0 + r or x > x1 - r) and (y < y0 + r or y > y1 - r)):
|
|
dx = x - (x0 + r if x < x0 + r else x1 - r)
|
|
dy = y - (y0 + r if y < y0 + r else y1 - r)
|
|
if dx * dx + dy * dy > r * r:
|
|
return False
|
|
return True
|
|
|
|
|
|
def fill_round(px, color, x0, y0, x1, y1, r=3):
|
|
for y in range(S):
|
|
for x in range(S):
|
|
if in_round_rect(x, y, x0, y0, x1, y1, r):
|
|
px[y][x] = color
|
|
|
|
|
|
def speckle(px, region, tones, seed=1, every=5):
|
|
"""Базовый тон + редкие акценты (1 из every) — без шахматного шума."""
|
|
x0, y0, x1, y1 = region
|
|
for y in range(y0, y1 + 1):
|
|
for x in range(x0, x1 + 1):
|
|
if px[y][x] is None:
|
|
continue
|
|
n = (x * 7 + y * 13 + seed) % every
|
|
px[y][x] = tones[1] if n == 0 else tones[0]
|
|
|
|
|
|
def hband(px, y0, y1, row_colors, r=3):
|
|
"""Горизонтальная полоса строго внутри внутреннего скругления."""
|
|
for y in range(y0, y1 + 1):
|
|
c = row_colors[y - y0]
|
|
for x in range(1, S - 1):
|
|
if in_round_rect(x, y, 1, 1, S - 2, S - 2, r):
|
|
px[y][x] = c
|
|
|
|
|
|
def stamp_letter(px, gx, gy, colors, outline=None):
|
|
"""Рисует LETTER_BIG в точке (gx, gy); colors — список цветов по строкам
|
|
буквы (1 цвет = весь ряд), outline — контур на 1 пиксель больше."""
|
|
h, w = len(LETTER_BIG), len(LETTER_BIG[0])
|
|
if outline:
|
|
for dy in range(-1, h + 1):
|
|
for dx in range(-1, w + 1):
|
|
inside = 0 <= dy < h and 0 <= dx < w and \
|
|
LETTER_BIG[dy][dx] == "X"
|
|
near = inside
|
|
if not inside:
|
|
for ey in (dy - 1, dy, dy + 1):
|
|
for ex in (dx - 1, dx, dx + 1):
|
|
if 0 <= ey < h and 0 <= ex < w and \
|
|
LETTER_BIG[ey][ex] == "X":
|
|
near = True
|
|
if near and not inside:
|
|
yy, xx = gy + dy, gx + dx
|
|
if 0 <= yy < S and 0 <= xx < S:
|
|
px[yy][xx] = outline
|
|
for dy, row in enumerate(LETTER_BIG):
|
|
for dx, ch in enumerate(row):
|
|
if ch == "X":
|
|
yy, xx = gy + dy, gx + dx
|
|
if 0 <= yy < S and 0 <= xx < S:
|
|
px[yy][xx] = colors[min(dy, len(colors) - 1)]
|
|
|
|
|
|
def variant_land():
|
|
"""Буква-как-блок: трава сверху, земля снизу, тёмный фон."""
|
|
px = canvas()
|
|
fill_round(px, FRAME, 0, 0, S - 1, S - 1, 4)
|
|
fill_round(px, DARK, 1, 1, S - 2, S - 2, 3)
|
|
# фоновая травяная полоса снизу (как на сайте)
|
|
hband(px, S - 7, S - 5, [GRASS_D, GRASS, GRASS])
|
|
speckle(px, (2, S - 7, S - 3, S - 5), (GRASS, GRASS_L), seed=3, every=5)
|
|
# буква: верх трава, низ земля, между ними тень-кромка
|
|
gx, gy = (S - 10) // 2, 5
|
|
colors = ([GRASS_L, GRASS_L] + [GRASS] * 3 + [GRASS_D] +
|
|
[DIRT] * 9 + [DIRT_D])
|
|
stamp_letter(px, gx, gy, colors, outline=INK)
|
|
return px
|
|
|
|
|
|
def variant_gold():
|
|
"""Золотая «P» на тёмном слоте с травяной полосой."""
|
|
px = canvas()
|
|
fill_round(px, FRAME, 0, 0, S - 1, S - 1, 4)
|
|
fill_round(px, DARK, 1, 1, S - 2, S - 2, 3)
|
|
gx, gy = (S - 10) // 2, 4
|
|
colors = [GOLD_L, GOLD_L] + [GOLD] * 11 + [GOLD_D] * 3
|
|
stamp_letter(px, gx, gy, colors, outline=INK)
|
|
# травяной край снизу
|
|
hband(px, S - 6, S - 4, [GRASS_D, GRASS, GRASS])
|
|
speckle(px, (2, S - 6, S - 3, S - 4), (GRASS, GRASS_L), seed=5, every=5)
|
|
return px
|
|
|
|
|
|
def variant_face():
|
|
"""Зелёный блок с тёмной «P» — родственник крипера."""
|
|
px = canvas()
|
|
fill_round(px, FRAME, 0, 0, S - 1, S - 1, 4)
|
|
fill_round(px, GRASS, 1, 1, S - 2, S - 2, 3)
|
|
speckle(px, (1, 1, S - 2, S - 2), (GRASS, GRASS_L), seed=7, every=6)
|
|
# грунт снизу — как у травяного блока (кромка, земля, тень)
|
|
hband(px, S - 9, S - 2, [GRASS_D] + [DIRT] * 6 + [DIRT_D])
|
|
speckle(px, (1, S - 9, S - 2, S - 2), (DIRT, DIRT_D), seed=11, every=4)
|
|
gx, gy = (S - 10) // 2, 5
|
|
stamp_letter(px, gx, gy, [INK] * 16, outline=None)
|
|
return px
|
|
|
|
|
|
VARIANTS = {"land": variant_land, "gold": variant_gold, "face": variant_face}
|
|
|
|
|
|
def rgba(c):
|
|
"""Всегда RGBA: полупрозрачность/прозрачность углов — 4 канала."""
|
|
return c if len(c) == 4 else (c[0], c[1], c[2], 255)
|
|
|
|
|
|
def write_png(path, px, k=K):
|
|
h = w = S * k
|
|
rows = []
|
|
for y in range(S):
|
|
row = []
|
|
for x in range(S):
|
|
c = px[y][x] or (0, 0, 0, 0)
|
|
row.append(rgba(c))
|
|
for _ in range(k):
|
|
line = bytearray(b"\x00")
|
|
for c in row:
|
|
line += bytes(c) * k
|
|
rows.append(bytes(line))
|
|
raw = b"".join(rows)
|
|
|
|
def chunk(tag, data):
|
|
return (struct.pack(">I", len(data)) + tag + data +
|
|
struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff))
|
|
|
|
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
|
|
with open(path, "wb") as f:
|
|
f.write(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) +
|
|
chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
|
|
return w, h
|
|
|
|
|
|
def preview(name, out_dir):
|
|
"""Строка из трёх вариантов в 256px + миниатюра 32px рядом (nearest)."""
|
|
tiles = []
|
|
for key in ("land", "gold", "face"):
|
|
px = VARIANTS[key]()
|
|
small = [[px[y][x] or (0, 0, 0, 0) for x in range(S)]
|
|
for y in range(S)]
|
|
tiles.append((px, small))
|
|
gap = 8
|
|
w = (S * K + gap + S + gap) * 3 + gap
|
|
h = S * K + gap * 2
|
|
buf = [[(40, 44, 60, 255)] * w for _ in range(h)]
|
|
ox = gap
|
|
for px, small in tiles:
|
|
for y in range(S * K):
|
|
for x in range(S * K):
|
|
c = px[y // K][x // K] or (0, 0, 0, 0)
|
|
buf[gap + y][ox + x] = rgba(c)
|
|
my = gap + (S * K - S) // 2
|
|
for y in range(S):
|
|
for x in range(S):
|
|
buf[my + y][ox + S * K + gap + x] = rgba(small[y][x])
|
|
ox += S * K + gap + S + gap
|
|
rows = [b"\x00" + bytes(v for p in row for v in p) for row in buf]
|
|
raw = b"".join(rows)
|
|
|
|
def chunk(tag, data):
|
|
return (struct.pack(">I", len(data)) + tag + data +
|
|
struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff))
|
|
|
|
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
|
|
with open(out_dir, "wb") as f:
|
|
f.write(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) +
|
|
chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b""))
|
|
return out_dir
|
|
|
|
|
|
if __name__ == "__main__":
|
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
if len(sys.argv) >= 3:
|
|
px = VARIANTS[sys.argv[1]]()
|
|
print(write_png(sys.argv[2], px))
|
|
else:
|
|
tmp = os.path.join(os.environ.get("TEMP", here), "logo_preview.png")
|
|
preview("all", tmp)
|
|
for key in VARIANTS:
|
|
out = os.path.join(os.environ.get("TEMP", here),
|
|
"logo_%s.png" % key)
|
|
print(key, write_png(out, VARIANTS[key]()))
|
|
print("preview:", tmp)
|