mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-22 14:00:36 +02:00
The product no longer carries Halo in its name; the game it plays is named only to say which disc image it needs. - The app: "Ringfall: Mjolnir VR", application id io.github.cesarus85.ringfall (the Java classes keep the decompilation's package), its update provider's authority from the id; the launcher's, updater's, host's and VR options' texts; the OpenXR application name. - New icon and Steam library pictures, a tilted, broken ring with a falling streak (tools/ringfall_art.py, from shapes and Overpass; the helmet and the HALO logo gone). - The Steam Frame: ~/Documents/RingfallMjolnirVR, RingfallMjolnirVR.apk, ringfall-mjolnir-vr.sh, Install-Ringfall-Mjolnir-VR.desktop and ringfall-mjolnir-vr-steam-frame.zip; the installer renames the folder of the builds before (~/Documents/HaloCE) and replaces their "Halo VR" shortcut and pictures. - Releases: only the Android builds, as ringfall-mjolnir-vr-android-*.zip (the updater's names), and the Frame's installer; the repository Cesarus85/ringfall-mjolnir-vr. - The signing key's default alias ringfall. - README.md is the project's own; the port's moved to docs/OPENCE_README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
6.7 KiB
Python
168 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Ringfall: Mjolnir VR's pictures, drawn from nothing but shapes and the
|
|
port's Overpass font (port/assets/fonts, SIL Open Font License):
|
|
|
|
python3 tools/ringfall_art.py
|
|
|
|
writes
|
|
- port/android/art/android-icon.png, the launcher icon's artwork (then run
|
|
tools/android_icon.py for the app's icon);
|
|
- port/android/steam_frame_grid/*.png, the Steam library's pictures of the
|
|
Steam Frame's shortcut (port/android/STEAM_FRAME.md): library.png
|
|
(920x430), libraryp.png (600x900), library_hero.png (1920x620) and
|
|
library_logo.png (800x300, transparent).
|
|
|
|
The emblem: a tilted ring, broken where a streak falls through it.
|
|
"""
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
FONT = ROOT / "port/assets/fonts/Overpass-900.ttf"
|
|
ICON = ROOT / "port/android/art/android-icon.png"
|
|
GRID = ROOT / "port/android/steam_frame_grid"
|
|
|
|
# the launcher icon's background (tools/android_icon.py keeps it)
|
|
ICON_BACKGROUND = (30, 83, 138)
|
|
LIGHT = (240, 246, 252)
|
|
ACCENT = (255, 176, 64)
|
|
TOP = (10, 34, 58)
|
|
BOTTOM = (6, 12, 24)
|
|
SUPERSAMPLE = 4
|
|
|
|
|
|
def gradient(size, top, bottom):
|
|
width, height = size
|
|
image = Image.new("RGBA", size)
|
|
draw = ImageDraw.Draw(image)
|
|
for y in range(height):
|
|
t = y / max(1, height - 1)
|
|
draw.line([(0, y), (width, y)], fill=tuple(int(a + (b - a) * t) for a, b in zip(top, bottom)) + (255,))
|
|
return image
|
|
|
|
|
|
def emblem(size, ring=LIGHT, streak=ACCENT):
|
|
"""the emblem on a transparent square of the size given"""
|
|
scale = size * SUPERSAMPLE
|
|
layer = Image.new("RGBA", (scale, scale), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(layer)
|
|
center = scale / 2
|
|
radius_x, radius_y = scale * 0.40, scale * 0.17
|
|
thickness = scale * 0.055
|
|
tilt = math.radians(-18)
|
|
# the ring as a band of quads along a tilted ellipse, with a gap at the
|
|
# top right where the streak passes
|
|
steps = 360
|
|
gap = (math.radians(35), math.radians(75))
|
|
def point(angle, grow):
|
|
x = (radius_x + grow) * math.cos(angle)
|
|
y = (radius_y + grow * radius_y / radius_x) * math.sin(angle)
|
|
return (center + x * math.cos(tilt) - y * math.sin(tilt), center + x * math.sin(tilt) + y * math.cos(tilt))
|
|
for step in range(steps):
|
|
a0 = 2 * math.pi * step / steps
|
|
a1 = 2 * math.pi * (step + 1.2) / steps
|
|
# (angles measured clockwise from the right, screen y down: the gap
|
|
# at the upper right is at negative angles)
|
|
if gap[0] <= (2 * math.pi - a0) % (2 * math.pi) <= gap[1]:
|
|
continue
|
|
draw.polygon([point(a0, -thickness / 2), point(a1, -thickness / 2), point(a1, thickness / 2),
|
|
point(a0, thickness / 2)], fill=ring)
|
|
# the streak: a tapering bar falling from the upper right through the gap
|
|
start = (center + scale * 0.40, center - scale * 0.42)
|
|
end = (center + scale * 0.02, center + scale * 0.12)
|
|
dx, dy = end[0] - start[0], end[1] - start[1]
|
|
length = math.hypot(dx, dy)
|
|
nx, ny = -dy / length, dx / length
|
|
head = scale * 0.050
|
|
draw.polygon([start, (end[0] + nx * head, end[1] + ny * head), (end[0] - nx * head, end[1] - ny * head)],
|
|
fill=streak)
|
|
draw.ellipse([end[0] - head, end[1] - head, end[0] + head, end[1] + head], fill=streak)
|
|
return layer.resize((size, size), Image.LANCZOS)
|
|
|
|
|
|
def italic_text(text, height, fill):
|
|
"""the text in Overpass Black, slanted, cropped to itself"""
|
|
font = ImageFont.truetype(str(FONT), int(height * SUPERSAMPLE))
|
|
left, top, right, bottom = font.getbbox(text)
|
|
width = right - left + int(height * SUPERSAMPLE * 0.4)
|
|
layer = Image.new("RGBA", (width, bottom - top + 8), (0, 0, 0, 0))
|
|
ImageDraw.Draw(layer).text((-left + int(height * SUPERSAMPLE * 0.2), -top), text, font=font, fill=fill)
|
|
slant = 0.18
|
|
layer = layer.transform((layer.width + int(layer.height * slant), layer.height), Image.AFFINE,
|
|
(1, slant, -layer.height * slant, 0, 1, 0), Image.BICUBIC)
|
|
layer = layer.crop(layer.getbbox())
|
|
return layer.resize((max(1, layer.width // SUPERSAMPLE), max(1, layer.height // SUPERSAMPLE)), Image.LANCZOS)
|
|
|
|
|
|
def logo(width):
|
|
""""RINGFALL" over "MJOLNIR VR", as wide as asked, transparent"""
|
|
title = italic_text("RINGFALL", 200, LIGHT)
|
|
title = title.resize((width, int(title.height * width / title.width)), Image.LANCZOS)
|
|
subtitle = italic_text("MJOLNIR VR", 200, ACCENT)
|
|
sub_width = int(width * 0.62)
|
|
subtitle = subtitle.resize((sub_width, int(subtitle.height * sub_width / subtitle.width)), Image.LANCZOS)
|
|
gap = int(title.height * 0.16)
|
|
image = Image.new("RGBA", (width, title.height + gap + subtitle.height), (0, 0, 0, 0))
|
|
image.alpha_composite(title, (0, 0))
|
|
image.alpha_composite(subtitle, (width - subtitle.width, title.height + gap))
|
|
return image
|
|
|
|
|
|
def shadowed(base, layer, position, blur=12):
|
|
shadow = Image.new("RGBA", base.size, (0, 0, 0, 0))
|
|
mask = Image.new("RGBA", layer.size, (0, 0, 0, 170))
|
|
mask.putalpha(layer.getchannel("A").point(lambda a: a * 2 // 3))
|
|
shadow.alpha_composite(mask, position)
|
|
base.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(blur)))
|
|
base.alpha_composite(layer, position)
|
|
|
|
|
|
def faded(layer, opacity):
|
|
layer = layer.copy()
|
|
layer.putalpha(layer.getchannel("A").point(lambda a: int(a * opacity)))
|
|
return layer
|
|
|
|
|
|
def main():
|
|
# the launcher icon: the light emblem on its solid background
|
|
icon = Image.new("RGBA", (512, 512), ICON_BACKGROUND + (255,))
|
|
icon.alpha_composite(emblem(400, LIGHT, LIGHT), (56, 56))
|
|
ICON.parent.mkdir(parents=True, exist_ok=True)
|
|
icon.convert("RGB").save(ICON, optimize=True)
|
|
print(ICON.relative_to(ROOT))
|
|
|
|
GRID.mkdir(parents=True, exist_ok=True)
|
|
|
|
# the wide capsule
|
|
image = gradient((920, 430), TOP, BOTTOM)
|
|
image.alpha_composite(faded(emblem(520), 0.35), (500, -40))
|
|
shadowed(image, logo(600), (70, 120))
|
|
image.save(GRID / "library.png", optimize=True)
|
|
|
|
# the tall capsule
|
|
image = gradient((600, 900), TOP, BOTTOM)
|
|
image.alpha_composite(emblem(420), (90, 130))
|
|
shadowed(image, logo(480), (60, 560))
|
|
image.save(GRID / "libraryp.png", optimize=True)
|
|
|
|
# the hero, under the logo: the emblem large and faint at the right
|
|
image = gradient((1920, 620), TOP, BOTTOM)
|
|
image.alpha_composite(faded(emblem(900), 0.45), (1080, -140))
|
|
image.save(GRID / "library_hero.png", optimize=True)
|
|
|
|
# the logo, over the hero
|
|
image = Image.new("RGBA", (800, 300), (0, 0, 0, 0))
|
|
mark = logo(560)
|
|
image.alpha_composite(mark, ((800 - mark.width) // 2, (300 - mark.height) // 2))
|
|
image.save(GRID / "library_logo.png", optimize=True)
|
|
for name in ("library.png", "libraryp.png", "library_hero.png", "library_logo.png"):
|
|
print((GRID / name).relative_to(ROOT))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|