datewise/brand/build_brand.py
metatroncubeswdev f96ab10634 feat(brand): brand assets at Shopify sizes; public landing, privacy and terms
- brand/: 1200x1200 full-bleed app icon, 1600x900 feature image, transparent logos,
  favicons, guidelines (palette, type, usage) and the build script
- / is now a branded landing page with no shop-domain form (App Store 2.3.1)
- /privacy and /terms describe Datewise's actual data handling and sub-processors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:26:12 -04:00

139 lines
6.8 KiB
Python

"""Build Datewise brand assets (Shopify sizes) from the generated source images.
usage: python -I build_brand.py <images_dir> <widget_screenshot> <out_brand_dir> <out_public_dir>
"""
import os
import shutil
import sys
import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont
SRC, SHOT, OUT, PUB = sys.argv[1:5]
TEAL = (13, 107, 93) # #0D6B5D
TEAL_DEEP = (4, 90, 81) # background of the reversed logo
SAGE = (232, 242, 237) # #E8F2ED
OFF_WHITE = (250, 251, 249) # #FAFBF9
FONT = os.path.expanduser(r"~/AppData/Local/Microsoft/Windows/Fonts/Inter-VariableFont_slnt,wght.ttf")
for d in ("source", "logo", "shopify", "web"):
os.makedirs(os.path.join(OUT, d), exist_ok=True)
os.makedirs(PUB, exist_ok=True)
src = lambda n: Image.open(os.path.join(SRC, f"{n}.png"))
out = lambda *p: os.path.join(OUT, *p)
# ---- originals, renamed -------------------------------------------------------------
for n, name in {1: "brand-board", 2: "app-icon-rounded-preview", 3: "logo-reversed-on-teal", 4: "logo-mono", 5: "symbol", 6: "logo"}.items():
shutil.copy(os.path.join(SRC, f"{n}.png"), out("source", f"{name}.png"))
def trim(im, pad=0):
box = im.getchannel("A").point(lambda v: 255 if v > 8 else 0).getbbox()
im = im.crop(box)
if pad:
c = Image.new("RGBA", (im.width + 2 * pad, im.height + 2 * pad), (0, 0, 0, 0))
c.paste(im, (pad, pad))
im = c
return im
# ---- transparent logos ----------------------------------------------------------------
logo = trim(src(6).convert("RGBA"), 24)
logo.save(out("logo", "datewise-logo.png"))
trim(src(4).convert("RGBA"), 24).save(out("logo", "datewise-logo-mono.png"))
symbol = trim(src(5).convert("RGBA"))
side = max(symbol.size) + 48
sq = Image.new("RGBA", (side, side), (0, 0, 0, 0))
sq.paste(symbol, ((side - symbol.width) // 2, (side - symbol.height) // 2))
sq.save(out("logo", "datewise-symbol.png"))
# White logo for dark backgrounds: alpha from distance to the teal background, colour from the source.
rev = np.asarray(src(3).convert("RGB")).astype(np.float32)
bg = np.array(TEAL_DEEP, np.float32)
dist = np.sqrt(((rev - bg) ** 2).sum(-1))
alpha = np.clip(dist / np.sqrt(((np.array([255, 255, 255]) - bg) ** 2).sum()) * 255 * 1.15, 0, 255).astype(np.uint8)
white = np.dstack([np.full(alpha.shape, 255, np.uint8)] * 3 + [alpha])
trim(Image.fromarray(white, "RGBA"), 24).save(out("logo", "datewise-logo-white.png"))
# ---- full-bleed app icon (1200x1200, no rounded corners, no padding) ------------------
icon = src(2).convert("RGB")
a = np.asarray(icon).astype(np.float32)
x0, y0, x1, y1 = 185, 185, 1070, 1070 # the rounded square
crop = a[y0:y1, x0:x1]
h, w, _ = crop.shape
yy, xx = np.mgrid[0:h, 0:w]
# Fit the teal gradient (quadratic in x, y) on pixels that are clearly teal background.
teal_mask = (crop[..., 1] > 60) & (crop[..., 1] < 125) & (crop[..., 0] < 25) & (abs(crop[..., 1] - crop[..., 2]) < 20)
content = np.zeros((h, w), bool)
content[340 - y0:885 - y0, 330 - x0:1005 - x0] = True
fit = teal_mask & ~content
X = np.stack([np.ones_like(xx), xx, yy, xx * xx, yy * yy, xx * yy], -1).astype(np.float64) / np.array([1, w, h, w * w, h * h, w * h])
coef = [np.linalg.lstsq(X[fit], crop[..., c][fit], rcond=None)[0] for c in range(3)]
model = np.stack([X @ coef[c] for c in range(3)], -1)
# Keep the original artwork only inside the content box; everything else is the smooth gradient.
dev = np.sqrt(((crop - model) ** 2).sum(-1))
m = np.where(content, np.clip((dev - 6) / 18, 0, 1), 0).astype(np.float32)
m = np.asarray(Image.fromarray((m * 255).astype(np.uint8)).filter(ImageFilter.MaxFilter(3)).filter(ImageFilter.GaussianBlur(1))).astype(np.float32) / 255
full = model * (1 - m[..., None]) + crop * m[..., None]
app_icon = Image.fromarray(np.clip(full, 0, 255).astype(np.uint8), "RGB").resize((1200, 1200), Image.LANCZOS)
app_icon.save(out("shopify", "app-icon-1200x1200.png"), optimize=True)
# ---- web icons ---------------------------------------------------------------------------
app_icon.resize((180, 180), Image.LANCZOS).save(out("web", "apple-touch-icon-180.png"))
app_icon.resize((512, 512), Image.LANCZOS).save(out("web", "icon-512.png"))
fav = app_icon.resize((256, 256), Image.LANCZOS)
fav.save(out("web", "favicon.ico"), sizes=[(16, 16), (32, 32), (48, 48)])
app_icon.resize((32, 32), Image.LANCZOS).save(out("web", "favicon-32.png"))
for f in ("favicon.ico", "apple-touch-icon-180.png"):
shutil.copy(out("web", f), os.path.join(PUB, f))
logo.save(os.path.join(PUB, "datewise-logo.png"))
# ---- feature image (1600x900): logo + one line + the real cart widget ------------------
def font(size, weight):
f = ImageFont.truetype(FONT, size)
try:
f.set_variation_by_axes([weight, 0] if len(f.get_variation_axes()) == 2 else [weight])
except Exception:
pass
return f
W, H = 1600, 900
feat = Image.new("RGB", (W, H), TEAL_DEEP)
d = ImageDraw.Draw(feat)
# Soft lighter wash top-left, echoing the icon's gradient.
wash = Image.new("L", (W, H), 0)
ImageDraw.Draw(wash).ellipse((-500, -600, 1000, 700), fill=70)
wash = wash.filter(ImageFilter.GaussianBlur(160))
feat = Image.composite(Image.new("RGB", (W, H), (20, 125, 110)), feat, wash)
d = ImageDraw.Draw(feat)
white_logo = Image.open(out("logo", "datewise-logo-white.png"))
lw = 560
white_logo = white_logo.resize((lw, round(white_logo.height * lw / white_logo.width)), Image.LANCZOS)
feat.paste(white_logo, (96, 300), white_logo)
d.text((120, 300 + white_logo.height + 28), "Customers choose their delivery\nor pickup time in the cart.", font=font(46, 600), fill=(255, 255, 255), spacing=14)
d.text((120, 300 + white_logo.height + 172), "Store pickup · Local delivery · Shipping", font=font(26, 450), fill=SAGE)
shot = Image.open(SHOT).convert("RGB")
panel = shot.crop((16, 228, 504, 1112)) # the scheduler panel in the e2e cart screenshot
ph = 780
panel = panel.resize((round(panel.width * ph / panel.height), ph), Image.LANCZOS)
card = Image.new("RGB", (panel.width + 48, panel.height + 48), (255, 255, 255))
card.paste(panel, (24, 24))
mask = Image.new("L", card.size, 0)
ImageDraw.Draw(mask).rounded_rectangle((0, 0, card.width - 1, card.height - 1), 28, fill=255)
cx, cy = W - card.width - 110, (H - card.height) // 2
shadow = Image.new("L", (W, H), 0)
ImageDraw.Draw(shadow).rounded_rectangle((cx + 8, cy + 24, cx + card.width + 8, cy + card.height + 24), 28, fill=110)
shadow = shadow.filter(ImageFilter.GaussianBlur(28))
feat = Image.composite(Image.new("RGB", (W, H), (2, 40, 36)), feat, shadow)
feat.paste(card, (cx, cy), mask)
feat.save(out("shopify", "feature-image-1600x900.png"), optimize=True)
for root, _, files in os.walk(OUT):
for f in sorted(files):
p = os.path.join(root, f)
try:
print(os.path.relpath(p, OUT), Image.open(p).size, f"{os.path.getsize(p) // 1024} KB")
except Exception:
print(os.path.relpath(p, OUT))