#!/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'\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''
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''
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")