#!/usr/bin/env python3 """HotDog CMS mark. Full mark = concept C "Live preview": the dog peeks over a window whose kitten page is half markup (blueprint), half rendered. Small cut = concept A "Peek": the dog over a plain window, for 40 px and below. Mustard ground. Writes every SVG into ../svg and renders the PNGs into ../png with rsvg-convert. Run from anywhere: python3 source/gen.py """ import json import os import subprocess import sys SRC = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, SRC) from base import (INK, MUSTARD, KETCHUP, PAGE, PAGE_LINE, BP_NAVY, BP_GRID, BP_LINE, # noqa: E402 dog, cat, paws, browser) ROOT = os.path.dirname(SRC) SVG = os.path.join(ROOT, "svg") PNG = os.path.join(ROOT, "png") GROUND = MUSTARD def full(p): """Concept C in the 128 grid. Head sits low so the chin tucks in behind the paws.""" x, y, w, h = 10, 60, 108, 64 defs = (f'' f'' f'') page_cat = "translate(64 96) scale(.38) translate(-64 -60)" bp = (f'' f'' f'{cat(page_cat, line=BP_LINE)}' f'' f'') rd = (f'{cat(page_cat)}' f'' f'') split = (f'' f'') return (f'{defs}' + dog("translate(64 40) scale(.64) translate(-64 -60)") + browser(x, y, w, h, body=bp + rd + split) + paws("translate(64 61) scale(.64) translate(-64 -88)")) def small(p): """Concept A for 40 px and below: bigger dog, heavier lines, no toes, dots or text lines.""" page = f'' return (dog("translate(64 58) scale(1) translate(-64 -60)", sw=1.2) + browser(10, 90, 108, 60, bar=9, body=page, dots=False, sw=1.2) + paws("translate(64 91) scale(1.04) translate(-64 -88)", toes=False, sw=1.2)) def svg(vb, inner, w=None, h=None, title=True): size = f' width="{w}" height="{h}"' if w else "" t = "HotDog CMS" if title else "" return f'{t}{inner}\n' def tile_inner(p, art, rx=28): # inset the art so the window's lower corners clear the tile's rounding return (f'' f'' f'{art(p)}') def tile(p, art, square=False, size=512): return svg("0 0 128 128", tile_inner(p, art, 0 if square else 28), size, size) # ---------------------------------------------------------------- wordmark (concept X2) # Outlines come from word.py (Recursive): 100-unit em, baseline y=0, y down. WORD = json.load(open(os.path.join(SRC, "word.json"))) THEMES = { # fg, split, pill, pill text "light": ("#1A1310", "#E0A81C", "#D93A2B", "#FFFDF8"), "dark": ("#FFF8EC", MUSTARD, MUSTARD, "#1A1310"), "mustard": ("#1A1310", KETCHUP, "#1A1310", MUSTARD), # on the mustard ground: ketchup split, ink pill } HOLLOW = 8 # stroke drawn inside "Hot" (clipped to the letters): 4 units of line MID = -27 # vertical centre the split and the pill sit on SPLIT_X, SPLIT_W, SPLIT_H = WORD["hot"]["advance"] + 6, 5.5, 102 DOG_X = SPLIT_X + SPLIT_W + 6 CMS_S = .38 PILL_X = DOG_X + WORD["dog"]["advance"] + 10 PILL_PX, PILL_H = .45 * 100 * CMS_S, 48 PILL_W = WORD["cms"]["advance"] * CMS_S + 2 * PILL_PX WORD_W = PILL_X + PILL_W WORD_TOP, WORD_BOT = MID - SPLIT_H / 2, WORD["dog"]["bounds"][3] def word(p, theme=None, mono=None): """The wordmark at the 100-unit em, origin at the left baseline. mono=colour draws it in one colour with "cms" knocked out of the pill.""" fg, split, pill, ptxt = (mono, mono, mono, None) if mono else THEMES[theme] cms_x = PILL_X + PILL_PX cms_base = MID + WORD["cms"]["xh"] * CMS_S / 2 cms = f'' f'{cms} fill="#000"/>') pill_svg = f'{knock}' else: pill_svg = f'{cms} fill="{ptxt}"/>' hot = WORD["hot"]["d"] return (f'' f'' f'' f'' + pill_svg) def wordmark(p, theme=None, mono=None): pad = 4 vb = f"{-pad} {WORD_TOP - pad:.2f} {WORD_W + 2 * pad:.2f} {WORD_BOT - WORD_TOP + 2 * pad:.2f}" w = 1000 return svg(vb, word(p, theme, mono), w, round(w * (WORD_BOT - WORD_TOP + 2 * pad) / (WORD_W + 2 * pad))) MARK_H = 150 # the tile beside the name is 1.5 em, as in the concept sheet def horizontal(p, theme, art=None, gap=32): art = art or full top = MID - MARK_H / 2 inner = (f'{tile_inner(p + "m", art)}' f'{word(p, theme)}') w = MARK_H + gap + WORD_W pad = 6 vb = f"{-pad} {top - pad:.2f} {w + 2 * pad:.2f} {MARK_H + 2 * pad}" return svg(vb, inner, 1280, round(1280 * (MARK_H + 2 * pad) / (w + 2 * pad))) def stacked(p, theme): m = 220 w = max(WORD_W, m) x_m = (w - m) / 2 y_w = m + 40 - WORD_TOP # name baseline: mark, a gap, then the top of the name inner = (f'{tile_inner(p + "m", full)}' f'{word(p, theme)}') h = y_w + WORD_BOT pad = 10 return svg(f"{-pad} {-pad} {w + 2 * pad:.2f} {h + 2 * pad:.2f}", inner, 720, round(720 * (h + 2 * pad) / (w + 2 * pad))) def txt(key, x, y, size, fill): """A run from word.json at a font size in px, (x, y) = left end of the baseline.""" return f'' def social(p): """1280x640 card, concept S2 "Big preview": a large version of mark C. On mustard, the dog peeks over a window whose left pane is the Markdown source and whose right pane is the rendered page; the name and the tagline sit top left.""" W, H = 1280, 640 x0, y0, x1, mid, bar = 80, 336, 1200, 640, 40 code = [("md_fence", 0), ("md_title", 1), ("md_fence", 2), ("md_h1", 4), ("md_p1", 6), ("md_p2", 7)] src = "".join(txt(k, x0 + 34, y0 + 76 + i * 30, 21, BP_LINE) for k, i in code) body = "#4A3D33" px, size = mid + 40, 24 ren = (txt("page_h1", px, y0 + 112, 46, INK) + txt("page_p1a", px, y0 + 166, size, body) + txt("page_p1b", px + WORD["page_p1a"]["advance"] * size / 100, y0 + 166, size, INK) + txt("page_p1c", px + (WORD["page_p1a"]["advance"] + WORD["page_p1b"]["advance"]) * size / 100, y0 + 166, size, body) + txt("page_p2", px, y0 + 200, size, body) + f'') dots = "".join(f'' for i, c in enumerate((KETCHUP, MUSTARD, "#7FBF6A"))) win = (f'' f'' f'' f'' f'{src}{ren}' f'' f'' f'{dots}' f'' f'') # the dog with its paws on the window's top edge: small-cut geometry, toes back on at this size pup = (f'' + dog("translate(64 58) scale(1) translate(-64 -60)") + paws("translate(64 91) scale(1.04) translate(-64 -88)") + '') name = f'{word(p, "mustard")}' return svg(f"0 0 {W} {H}", f'{win}{pup}{name}' + txt("tagline", 84, 244, 26, INK), W, H, title=False) def flat(p): """Full mark on transparent; the window closes at the bottom, so nothing is cut.""" return svg("4 14 120 114", full(p), 400, 380) def small_flat(p): return svg("8 22 112 112", f'' f'{small(p)}', 64, 64) def avatar(p): """Full-bleed square for round crops: the window runs off the bottom, so a circle crop curves it.""" return svg("0 0 128 128", f'' f'{small(p)}', 512, 512) def write(name, text): with open(os.path.join(SVG, name), "w") as f: f.write(text) def render(src, out, w, h=None): cmd = ["rsvg-convert", "-w", str(w)] + (["-h", str(h)] if h else []) subprocess.run(cmd + [os.path.join(SVG, src), "-o", os.path.join(PNG, out)], check=True) if __name__ == "__main__": os.makedirs(SVG, exist_ok=True) os.makedirs(PNG, exist_ok=True) write("icon-tile.svg", tile("ct", full)) write("icon-tile-square.svg", tile("cq", full, square=True)) write("icon-flat.svg", flat("cf")) write("icon-small.svg", small_flat("cs")) write("favicon.svg", tile("fv", small, size=64)) write("avatar.svg", avatar("av")) for s in (16, 32, 48): render("favicon.svg", f"favicon-{s}.png", s) render("icon-tile-square.svg", "favicon-180.png", 180) # apple-touch: iOS rounds it render("icon-tile.svg", "favicon-192.png", 192) render("icon-tile.svg", "favicon-512.png", 512) for s in (512, 1024, 2048): render("icon-tile.svg", f"icon-tile-{s}.png", s) render("icon-flat.svg", "icon-flat-1024.png", 1024) render("avatar.svg", "avatar-512.png", 512) for th in ("light", "dark"): write(f"wordmark-{th}.svg", wordmark(f"w{th[0]}", th)) write(f"logo-horizontal-{th}.svg", horizontal(f"h{th[0]}", th)) write(f"logo-header-{th}.svg", horizontal(f"r{th[0]}", th, art=small, gap=36)) write(f"logo-stacked-{th}.svg", stacked(f"s{th[0]}", th)) render(f"wordmark-{th}.svg", f"wordmark-{th}.png", 1280) render(f"logo-horizontal-{th}.svg", f"logo-horizontal-{th}.png", 1280) render(f"logo-header-{th}.svg", f"logo-header-{th}.png", 640) render(f"logo-stacked-{th}.svg", f"logo-stacked-{th}.png", 720) write("wordmark-mono-black.svg", wordmark("mk", mono="#111315")) write("wordmark-mono-white.svg", wordmark("mw", mono="#FFFFFF")) write("social-preview.svg", social("so")) render("social-preview.svg", "social-preview.png", 1280, 640) print("hotdog bundle written:", len(os.listdir(SVG)), "svg,", len(os.listdir(PNG)), "png")