Re-worked assets
3
branding/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
*.tmp
|
|
||||||
.DS_Store
|
|
||||||
*.log
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable, TypedDict
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
|
|
||||||
class Polygon(TypedDict):
|
|
||||||
coords: list[tuple[float, float]]
|
|
||||||
fill: str
|
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).parent
|
|
||||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
||||||
SOURCE_DIR = SCRIPT_DIR / "source"
|
|
||||||
LOGOS_DIR = SCRIPT_DIR / "logos"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_svg_polygons(svg_path: Path) -> list[Polygon]:
|
|
||||||
tree = ET.parse(svg_path)
|
|
||||||
root = tree.getroot()
|
|
||||||
|
|
||||||
ns = {"svg": "http://www.w3.org/2000/svg"}
|
|
||||||
polygons = root.findall(".//svg:polygon", ns)
|
|
||||||
if not polygons:
|
|
||||||
polygons = root.findall(".//polygon")
|
|
||||||
|
|
||||||
result: list[Polygon] = []
|
|
||||||
for poly in polygons:
|
|
||||||
points_str = poly.get("points", "").strip()
|
|
||||||
fill = poly.get("fill", "#000000")
|
|
||||||
|
|
||||||
coords: list[tuple[float, float]] = []
|
|
||||||
for pair in points_str.split():
|
|
||||||
x, y = pair.split(",")
|
|
||||||
coords.append((float(x), float(y)))
|
|
||||||
|
|
||||||
result.append({"coords": coords, "fill": fill})
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_bbox(polygons: list[Polygon]) -> dict[str, float]:
|
|
||||||
all_coords: list[tuple[float, float]] = []
|
|
||||||
for poly in polygons:
|
|
||||||
all_coords.extend(poly["coords"])
|
|
||||||
|
|
||||||
xs = [c[0] for c in all_coords]
|
|
||||||
ys = [c[1] for c in all_coords]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"min_x": min(xs),
|
|
||||||
"max_x": max(xs),
|
|
||||||
"min_y": min(ys),
|
|
||||||
"max_y": max(ys),
|
|
||||||
"width": max(xs) - min(xs),
|
|
||||||
"height": max(ys) - min(ys),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def scale_and_center(
|
|
||||||
polygons: list[Polygon], viewbox_size: float, target_width: float
|
|
||||||
) -> list[Polygon]:
|
|
||||||
bbox = get_bbox(polygons)
|
|
||||||
|
|
||||||
scale = target_width / bbox["width"]
|
|
||||||
center = viewbox_size / 2
|
|
||||||
|
|
||||||
scaled_polys: list[Polygon] = []
|
|
||||||
for poly in polygons:
|
|
||||||
scaled_coords = [(x * scale, y * scale) for x, y in poly["coords"]]
|
|
||||||
scaled_polys.append({"coords": scaled_coords, "fill": poly["fill"]})
|
|
||||||
|
|
||||||
scaled_bbox = get_bbox(scaled_polys)
|
|
||||||
current_center_x = (scaled_bbox["min_x"] + scaled_bbox["max_x"]) / 2
|
|
||||||
current_center_y = (scaled_bbox["min_y"] + scaled_bbox["max_y"]) / 2
|
|
||||||
|
|
||||||
offset_x = center - current_center_x
|
|
||||||
offset_y = center - current_center_y
|
|
||||||
|
|
||||||
final_polys: list[Polygon] = []
|
|
||||||
for poly in scaled_polys:
|
|
||||||
final_coords = [(x + offset_x, y + offset_y) for x, y in poly["coords"]]
|
|
||||||
final_polys.append({"coords": final_coords, "fill": poly["fill"]})
|
|
||||||
|
|
||||||
return final_polys
|
|
||||||
|
|
||||||
|
|
||||||
def format_svg_points(coords: list[tuple[float, float]]) -> str:
|
|
||||||
return " ".join(f"{x:.3f},{y:.3f}" for x, y in coords)
|
|
||||||
|
|
||||||
|
|
||||||
def format_android_path(coords: list[tuple[float, float]]) -> str:
|
|
||||||
points = " ".join(f"{x:.3f},{y:.3f}" for x, y in coords)
|
|
||||||
pairs = points.split()
|
|
||||||
return f"M{pairs[0]} L{pairs[1]} L{pairs[2]} Z"
|
|
||||||
|
|
||||||
|
|
||||||
def generate_svg(polygons: list[Polygon], width: int, height: int) -> str:
|
|
||||||
lines = [
|
|
||||||
f'<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">'
|
|
||||||
]
|
|
||||||
for poly in polygons:
|
|
||||||
points = format_svg_points(poly["coords"])
|
|
||||||
lines.append(f' <polygon points="{points}" fill="{poly["fill"]}"/>')
|
|
||||||
lines.append("</svg>")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_android_vector(
|
|
||||||
polygons: list[Polygon], width: int, height: int, viewbox: int
|
|
||||||
) -> str:
|
|
||||||
lines = [
|
|
||||||
'<?xml version="1.0" encoding="utf-8"?>',
|
|
||||||
'<vector xmlns:android="http://schemas.android.com/apk/res/android"',
|
|
||||||
f' android:width="{width}dp"',
|
|
||||||
f' android:height="{height}dp"',
|
|
||||||
f' android:viewportWidth="{viewbox}"',
|
|
||||||
f' android:viewportHeight="{viewbox}">',
|
|
||||||
]
|
|
||||||
for poly in polygons:
|
|
||||||
path = format_android_path(poly["coords"])
|
|
||||||
lines.append(
|
|
||||||
f' <path android:fillColor="{poly["fill"]}" android:pathData="{path}" />'
|
|
||||||
)
|
|
||||||
lines.append("</vector>")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def rasterize_svg(
|
|
||||||
svg_path: Path,
|
|
||||||
output_path: Path,
|
|
||||||
size: int,
|
|
||||||
bg_color: tuple[int, int, int, int] | None = None,
|
|
||||||
circular: bool = False,
|
|
||||||
) -> None:
|
|
||||||
from xml.dom import minidom
|
|
||||||
|
|
||||||
doc = minidom.parse(str(svg_path))
|
|
||||||
|
|
||||||
img = Image.new(
|
|
||||||
"RGBA", (size, size), (255, 255, 255, 0) if bg_color is None else bg_color
|
|
||||||
)
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
svg_elem = doc.getElementsByTagName("svg")[0]
|
|
||||||
viewbox = svg_elem.getAttribute("viewBox").split()
|
|
||||||
if viewbox:
|
|
||||||
vb_width = float(viewbox[2])
|
|
||||||
vb_height = float(viewbox[3])
|
|
||||||
scale_x = size / vb_width
|
|
||||||
scale_y = size / vb_height
|
|
||||||
else:
|
|
||||||
scale_x = scale_y = 1
|
|
||||||
|
|
||||||
def parse_transform(
|
|
||||||
transform_str: str,
|
|
||||||
) -> Callable[[float, float], tuple[float, float]]:
|
|
||||||
import re
|
|
||||||
|
|
||||||
if not transform_str:
|
|
||||||
return lambda x, y: (x, y)
|
|
||||||
|
|
||||||
transforms: list[tuple[str, list[float]]] = []
|
|
||||||
for match in re.finditer(r"(\w+)\(([^)]+)\)", transform_str):
|
|
||||||
func, args_str = match.groups()
|
|
||||||
args = [float(x) for x in args_str.replace(",", " ").split()]
|
|
||||||
transforms.append((func, args))
|
|
||||||
|
|
||||||
def apply_transforms(x: float, y: float) -> tuple[float, float]:
|
|
||||||
for func, args in transforms:
|
|
||||||
if func == "translate":
|
|
||||||
x += args[0]
|
|
||||||
y += args[1] if len(args) > 1 else args[0]
|
|
||||||
elif func == "scale":
|
|
||||||
x *= args[0]
|
|
||||||
y *= args[1] if len(args) > 1 else args[0]
|
|
||||||
return x, y
|
|
||||||
|
|
||||||
return apply_transforms
|
|
||||||
|
|
||||||
for g in doc.getElementsByTagName("g"):
|
|
||||||
transform = parse_transform(g.getAttribute("transform"))
|
|
||||||
|
|
||||||
for poly in g.getElementsByTagName("polygon"):
|
|
||||||
points_str = poly.getAttribute("points").strip()
|
|
||||||
fill = poly.getAttribute("fill")
|
|
||||||
if not fill:
|
|
||||||
fill = "#000000"
|
|
||||||
|
|
||||||
coords: list[tuple[float, float]] = []
|
|
||||||
for pair in points_str.split():
|
|
||||||
x, y = pair.split(",")
|
|
||||||
x, y = float(x), float(y)
|
|
||||||
x, y = transform(x, y)
|
|
||||||
coords.append((x * scale_x, y * scale_y))
|
|
||||||
|
|
||||||
draw.polygon(coords, fill=fill)
|
|
||||||
|
|
||||||
for poly in doc.getElementsByTagName("polygon"):
|
|
||||||
if poly.parentNode and getattr(poly.parentNode, "tagName", None) == "g":
|
|
||||||
continue
|
|
||||||
|
|
||||||
points_str = poly.getAttribute("points").strip()
|
|
||||||
fill = poly.getAttribute("fill")
|
|
||||||
if not fill:
|
|
||||||
fill = "#000000"
|
|
||||||
|
|
||||||
coords = []
|
|
||||||
for pair in points_str.split():
|
|
||||||
x, y = pair.split(",")
|
|
||||||
coords.append((float(x) * scale_x, float(y) * scale_y))
|
|
||||||
|
|
||||||
draw.polygon(coords, fill=fill)
|
|
||||||
|
|
||||||
if circular:
|
|
||||||
mask = Image.new("L", (size, size), 0)
|
|
||||||
mask_draw = ImageDraw.Draw(mask)
|
|
||||||
mask_draw.ellipse((0, 0, size, size), fill=255)
|
|
||||||
img.putalpha(mask)
|
|
||||||
|
|
||||||
img.save(output_path)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
print("Generating branding assets...")
|
|
||||||
|
|
||||||
logo_svg = SOURCE_DIR / "logo.svg"
|
|
||||||
icon_light = SOURCE_DIR / "icon-light.svg"
|
|
||||||
icon_dark = SOURCE_DIR / "icon-dark.svg"
|
|
||||||
icon_tinted = SOURCE_DIR / "icon-tinted.svg"
|
|
||||||
|
|
||||||
polygons = parse_svg_polygons(logo_svg)
|
|
||||||
|
|
||||||
print(" iOS...")
|
|
||||||
ios_assets = PROJECT_ROOT / "ios/Ascently/Assets.xcassets/AppIcon.appiconset"
|
|
||||||
|
|
||||||
for src, dst in [
|
|
||||||
(icon_light, ios_assets / "app_icon_light_template.svg"),
|
|
||||||
(icon_dark, ios_assets / "app_icon_dark_template.svg"),
|
|
||||||
(icon_tinted, ios_assets / "app_icon_tinted_template.svg"),
|
|
||||||
]:
|
|
||||||
with open(src) as f:
|
|
||||||
content = f.read()
|
|
||||||
with open(dst, "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
img_light = Image.new("RGB", (1024, 1024), (255, 255, 255))
|
|
||||||
draw_light = ImageDraw.Draw(img_light)
|
|
||||||
scaled = scale_and_center(polygons, 1024, int(1024 * 0.7))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_light.polygon(coords, fill=poly["fill"])
|
|
||||||
img_light.save(ios_assets / "app_icon_1024.png")
|
|
||||||
|
|
||||||
img_dark = Image.new("RGB", (1024, 1024), (26, 26, 26))
|
|
||||||
draw_dark = ImageDraw.Draw(img_dark)
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_dark.polygon(coords, fill=poly["fill"])
|
|
||||||
img_dark.save(ios_assets / "app_icon_1024_dark.png")
|
|
||||||
|
|
||||||
img_tinted = Image.new("RGB", (1024, 1024), (0, 0, 0))
|
|
||||||
draw_tinted = ImageDraw.Draw(img_tinted)
|
|
||||||
for i, poly in enumerate(scaled):
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_tinted.polygon(coords, fill=(0, 0, 0))
|
|
||||||
img_tinted.save(ios_assets / "app_icon_1024_tinted.png")
|
|
||||||
|
|
||||||
print(" Android...")
|
|
||||||
|
|
||||||
polys_108 = scale_and_center(polygons, 108, 60)
|
|
||||||
android_xml = generate_android_vector(polys_108, 108, 108, 108)
|
|
||||||
(
|
|
||||||
PROJECT_ROOT / "android/app/src/main/res/drawable/ic_launcher_foreground.xml"
|
|
||||||
).write_text(android_xml)
|
|
||||||
|
|
||||||
polys_24 = scale_and_center(polygons, 24, 20)
|
|
||||||
mountains_xml = generate_android_vector(polys_24, 24, 24, 24)
|
|
||||||
(PROJECT_ROOT / "android/app/src/main/res/drawable/ic_mountains.xml").write_text(
|
|
||||||
mountains_xml
|
|
||||||
)
|
|
||||||
|
|
||||||
for density, size in [
|
|
||||||
("mdpi", 48),
|
|
||||||
("hdpi", 72),
|
|
||||||
("xhdpi", 96),
|
|
||||||
("xxhdpi", 144),
|
|
||||||
("xxxhdpi", 192),
|
|
||||||
]:
|
|
||||||
mipmap_dir = PROJECT_ROOT / f"android/app/src/main/res/mipmap-{density}"
|
|
||||||
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.6))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(mipmap_dir / "ic_launcher.webp")
|
|
||||||
|
|
||||||
img_round = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
|
||||||
draw_round = ImageDraw.Draw(img_round)
|
|
||||||
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw_round.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
mask = Image.new("L", (size, size), 0)
|
|
||||||
mask_draw = ImageDraw.Draw(mask)
|
|
||||||
mask_draw.ellipse((0, 0, size, size), fill=255)
|
|
||||||
img_round.putalpha(mask)
|
|
||||||
|
|
||||||
img_round.save(mipmap_dir / "ic_launcher_round.webp")
|
|
||||||
|
|
||||||
print(" Docs...")
|
|
||||||
|
|
||||||
polys_32 = scale_and_center(polygons, 32, 26)
|
|
||||||
logo_svg_32 = generate_svg(polys_32, 32, 32)
|
|
||||||
(PROJECT_ROOT / "docs/src/assets/logo.svg").write_text(logo_svg_32)
|
|
||||||
(PROJECT_ROOT / "docs/src/assets/logo-dark.svg").write_text(logo_svg_32)
|
|
||||||
|
|
||||||
polys_256 = scale_and_center(polygons, 256, 208)
|
|
||||||
logo_svg_256 = generate_svg(polys_256, 256, 256)
|
|
||||||
(PROJECT_ROOT / "docs/src/assets/logo-highres.svg").write_text(logo_svg_256)
|
|
||||||
|
|
||||||
logo_32_path = PROJECT_ROOT / "docs/src/assets/logo.svg"
|
|
||||||
rasterize_svg(logo_32_path, PROJECT_ROOT / "docs/public/favicon.png", 32)
|
|
||||||
|
|
||||||
sizes = [16, 32, 48]
|
|
||||||
imgs = []
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 0))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
imgs.append(img)
|
|
||||||
|
|
||||||
imgs[0].save(
|
|
||||||
PROJECT_ROOT / "docs/public/favicon.ico",
|
|
||||||
format="ICO",
|
|
||||||
sizes=[(s, s) for s in sizes],
|
|
||||||
append_images=imgs[1:],
|
|
||||||
)
|
|
||||||
|
|
||||||
print(" Logos...")
|
|
||||||
|
|
||||||
LOGOS_DIR.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
sizes = [64, 128, 256, 512, 1024, 2048]
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 0))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(LOGOS_DIR / f"logo-{size}.png")
|
|
||||||
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(LOGOS_DIR / f"logo-{size}-white.png")
|
|
||||||
|
|
||||||
for size in sizes:
|
|
||||||
img = Image.new("RGBA", (size, size), (26, 26, 26, 255))
|
|
||||||
draw = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
scaled = scale_and_center(polygons, size, int(size * 0.8))
|
|
||||||
for poly in scaled:
|
|
||||||
coords = [(x, y) for x, y in poly["coords"]]
|
|
||||||
draw.polygon(coords, fill=poly["fill"])
|
|
||||||
|
|
||||||
img.save(LOGOS_DIR / f"logo-{size}-dark.png")
|
|
||||||
|
|
||||||
print("Done.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
if ! command -v python3 &> /dev/null; then
|
|
||||||
echo "Error: Python 3 required"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 "$SCRIPT_DIR/generate.py"
|
|
||||||
BIN
branding/source/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
branding/source/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
branding/source/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
branding/source/Balls.icon/Assets/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
67
branding/source/Balls.icon/icon.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"fill" : "automatic",
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyRedBall.png",
|
||||||
|
"name" : "AscentlyRedBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.4,
|
||||||
|
"translation-in-points" : [
|
||||||
|
90.60312499999992,
|
||||||
|
127.86484375000009
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyYellowBall.png",
|
||||||
|
"name" : "AscentlyYellowBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.3,
|
||||||
|
"translation-in-points" : [
|
||||||
|
90.50312500000001,
|
||||||
|
-177.66484375
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyBlueBall.png",
|
||||||
|
"name" : "AscentlyBlueBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.3,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-138.20312500000006,
|
||||||
|
177.3648437500001
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyGreenBall.png",
|
||||||
|
"name" : "AscentlyGreenBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.2,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-138.30312499999997,
|
||||||
|
-43.08515625000001
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
branding/source/Icon.icon/Assets/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
branding/source/Icon.icon/Assets/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
45
branding/source/Icon.icon/icon.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"fill" : "automatic",
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"image-name" : "AscetlyTriangle2.png",
|
||||||
|
"name" : "AscetlyTriangle2",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.75,
|
||||||
|
"translation-in-points" : [
|
||||||
|
108,
|
||||||
|
-53.8125
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscetlyTriangle1.png",
|
||||||
|
"name" : "AscetlyTriangle1",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.5,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-215,
|
||||||
|
39.9375
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -460,7 +460,7 @@
|
|||||||
D24C19742E75002A0045894C /* Debug */ = {
|
D24C19742E75002A0045894C /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
@@ -508,7 +508,7 @@
|
|||||||
D24C19752E75002A0045894C /* Release */ = {
|
D24C19752E75002A0045894C /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"filename": "app_icon_1024.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"platform": "ios",
|
|
||||||
"size": "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"filename": "app_icon_1024_dark.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"platform": "ios",
|
|
||||||
"size": "1024x1024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "tinted"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"filename": "app_icon_1024_tinted.png",
|
|
||||||
"idiom": "universal",
|
|
||||||
"platform": "ios",
|
|
||||||
"size": "1024x1024"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"author": "xcode",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#1A1A1A" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="#FFFFFF" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#FFC107"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#F44336"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="1024" height="1024" fill="transparent" rx="180" ry="180"/>
|
|
||||||
<g transform="translate(512, 512) scale(4.75) translate(-54, -42.5)">
|
|
||||||
<polygon points="8,75 35,14.25 62,75" fill="#000000" opacity="0.8"/>
|
|
||||||
<polygon points="31.25,75 65,0.75 98.75,75" fill="#000000" opacity="0.9"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 443 B |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyBlueBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyGreenBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyRedBall.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
ios/Ascently/Balls.icon/Assets/AscentlyYellowBall.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
67
ios/Ascently/Balls.icon/icon.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"fill" : "automatic",
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyRedBall.png",
|
||||||
|
"name" : "AscentlyRedBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.4,
|
||||||
|
"translation-in-points" : [
|
||||||
|
90.60312499999992,
|
||||||
|
127.86484375000009
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyYellowBall.png",
|
||||||
|
"name" : "AscentlyYellowBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.3,
|
||||||
|
"translation-in-points" : [
|
||||||
|
90.50312500000001,
|
||||||
|
-177.66484375
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyBlueBall.png",
|
||||||
|
"name" : "AscentlyBlueBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.3,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-138.20312500000006,
|
||||||
|
177.3648437500001
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscentlyGreenBall.png",
|
||||||
|
"name" : "AscentlyGreenBall",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.2,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-138.30312499999997,
|
||||||
|
-43.08515625000001
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Ascently/Icon.icon/Assets/AscetlyTriangle1.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
ios/Ascently/Icon.icon/Assets/AscetlyTriangle2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
45
ios/Ascently/Icon.icon/icon.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"fill" : "automatic",
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"image-name" : "AscetlyTriangle2.png",
|
||||||
|
"name" : "AscetlyTriangle2",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.75,
|
||||||
|
"translation-in-points" : [
|
||||||
|
108,
|
||||||
|
-53.8125
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "AscetlyTriangle1.png",
|
||||||
|
"name" : "AscetlyTriangle1",
|
||||||
|
"position" : {
|
||||||
|
"scale" : 0.5,
|
||||||
|
"translation-in-points" : [
|
||||||
|
-215,
|
||||||
|
39.9375
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -321,7 +321,7 @@ import SwiftUI
|
|||||||
private func validateAssetConfiguration() {
|
private func validateAssetConfiguration() {
|
||||||
// Check if main bundle contains the expected icon assets
|
// Check if main bundle contains the expected icon assets
|
||||||
let expectedAssets = [
|
let expectedAssets = [
|
||||||
"AppIcon",
|
"Icon",
|
||||||
"AppLogo",
|
"AppLogo",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ extension SheetType: Identifiable {
|
|||||||
|
|
||||||
struct AppearanceSection: View {
|
struct AppearanceSection: View {
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
|
|
||||||
let columns = [
|
let columns = [
|
||||||
GridItem(.adaptive(minimum: 44))
|
GridItem(.adaptive(minimum: 44))
|
||||||
]
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section("Appearance") {
|
Section("Appearance") {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
@@ -96,7 +96,7 @@ struct AppearanceSection: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
|
|
||||||
LazyVGrid(columns: columns, spacing: 12) {
|
LazyVGrid(columns: columns, spacing: 12) {
|
||||||
ForEach(ThemeManager.presetColors, id: \.self) { color in
|
ForEach(ThemeManager.presetColors, id: \.self) { color in
|
||||||
Circle()
|
Circle()
|
||||||
@@ -123,7 +123,7 @@ struct AppearanceSection: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isSelected(.blue) {
|
if !isSelected(.blue) {
|
||||||
Button("Reset to Default") {
|
Button("Reset to Default") {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
@@ -134,17 +134,17 @@ struct AppearanceSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isSelected(_ color: Color) -> Bool {
|
private func isSelected(_ color: Color) -> Bool {
|
||||||
// Compare using UIColor to handle different Color initializers
|
// Compare using UIColor to handle different Color initializers
|
||||||
let selectedUIColor = UIColor(themeManager.accentColor)
|
let selectedUIColor = UIColor(themeManager.accentColor)
|
||||||
let targetUIColor = UIColor(color)
|
let targetUIColor = UIColor(color)
|
||||||
|
|
||||||
// Simple equality check might fail for some system colors, so we check components if needed
|
// Simple equality check might fail for some system colors, so we check components if needed
|
||||||
// But usually UIColor equality is robust enough for system colors
|
// But usually UIColor equality is robust enough for system colors
|
||||||
return selectedUIColor == targetUIColor
|
return selectedUIColor == targetUIColor
|
||||||
}
|
}
|
||||||
|
|
||||||
private func colorDescription(for color: Color) -> String {
|
private func colorDescription(for color: Color) -> String {
|
||||||
switch color {
|
switch color {
|
||||||
case .blue: return "Blue"
|
case .blue: return "Blue"
|
||||||
@@ -809,12 +809,12 @@ struct SyncSettingsView: View {
|
|||||||
|
|
||||||
syncService.serverURL = newURL
|
syncService.serverURL = newURL
|
||||||
syncService.authToken = newToken
|
syncService.authToken = newToken
|
||||||
|
|
||||||
// Ensure provider type is set to server
|
// Ensure provider type is set to server
|
||||||
if syncService.providerType != .server {
|
if syncService.providerType != .server {
|
||||||
syncService.providerType = .server
|
syncService.providerType = .server
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
@@ -1116,7 +1116,7 @@ struct HealthKitSection: View {
|
|||||||
|
|
||||||
struct MusicSection: View {
|
struct MusicSection: View {
|
||||||
@EnvironmentObject var musicService: MusicService
|
@EnvironmentObject var musicService: MusicService
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
Toggle(isOn: Binding(
|
Toggle(isOn: Binding(
|
||||||
@@ -1129,7 +1129,7 @@ struct MusicSection: View {
|
|||||||
Text("Apple Music Integration")
|
Text("Apple Music Integration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if musicService.isMusicEnabled {
|
if musicService.isMusicEnabled {
|
||||||
if !musicService.isAuthorized {
|
if !musicService.isAuthorized {
|
||||||
Button("Connect Apple Music") {
|
Button("Connect Apple Music") {
|
||||||
@@ -1140,14 +1140,14 @@ struct MusicSection: View {
|
|||||||
} else {
|
} else {
|
||||||
Toggle("Auto-Play on Session Start", isOn: $musicService.isAutoPlayEnabled)
|
Toggle("Auto-Play on Session Start", isOn: $musicService.isAutoPlayEnabled)
|
||||||
Toggle("Stop Music on Session End", isOn: $musicService.isAutoStopEnabled)
|
Toggle("Stop Music on Session End", isOn: $musicService.isAutoStopEnabled)
|
||||||
|
|
||||||
Picker("Playlist", selection: $musicService.selectedPlaylistId) {
|
Picker("Playlist", selection: $musicService.selectedPlaylistId) {
|
||||||
Text("None").tag(nil as String?)
|
Text("None").tag(nil as String?)
|
||||||
ForEach(musicService.playlists, id: \.id) { playlist in
|
ForEach(musicService.playlists, id: \.id) { playlist in
|
||||||
Text(playlist.name).tag(playlist.id.rawValue as String?)
|
Text(playlist.name).tag(playlist.id.rawValue as String?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if musicService.isAutoPlayEnabled {
|
if musicService.isAutoPlayEnabled {
|
||||||
Text("Music will only auto-play if headphones are connected when you start a session.")
|
Text("Music will only auto-play if headphones are connected when you start a session.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||