Files

252 lines
13 KiB
Python

#!/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'<clipPath id="{p}L"><rect x="{x}" y="{y}" width="{64 - x}" height="{h}"/></clipPath>'
f'<clipPath id="{p}R"><rect x="64" y="{y}" width="{x + w - 64}" height="{h}"/></clipPath>'
f'<pattern id="{p}g" width="6" height="6" patternUnits="userSpaceOnUse"><path d="M6 0 H0 V6" fill="none" stroke="{BP_GRID}" stroke-opacity=".28" stroke-width=".6"/></pattern>')
page_cat = "translate(64 96) scale(.38) translate(-64 -60)"
bp = (f'<g clip-path="url(#{p}L)"><rect x="{x}" y="{y}" width="{w}" height="{h}" fill="{BP_NAVY}"/>'
f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="url(#{p}g)"/>'
f'{cat(page_cat, line=BP_LINE)}'
f'<rect x="20" y="76" width="34" height="5" rx="1" fill="none" stroke="{BP_LINE}" stroke-width="1.2" stroke-dasharray="2 1.6"/>'
f'<path d="M20 113 H57 M20 119 H50" stroke="{BP_LINE}" stroke-width="1.4" stroke-dasharray="2.5 2" stroke-linecap="round"/></g>')
rd = (f'<g clip-path="url(#{p}R)">{cat(page_cat)}'
f'<rect x="70" y="76" width="38" height="5" rx="2.5" fill="{KETCHUP}"/>'
f'<path d="M71 113 H108 M71 119 H98" stroke="{PAGE_LINE}" stroke-width="3" stroke-linecap="round"/></g>')
split = (f'<path d="M64 {y + 11} V{y + h}" stroke="{MUSTARD}" stroke-width="2.6"/>'
f'<circle cx="64" cy="{y + 55}" r="4.6" fill="{MUSTARD}" stroke="{INK}" stroke-width="2"/>')
return (f'<defs>{defs}</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'<rect x="22" y="106" width="44" height="7" rx="3.5" fill="{KETCHUP}"/>'
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 = "<title>HotDog CMS</title>" if title else ""
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{vb}"{size} role="img" aria-label="HotDog CMS">{t}{inner}</svg>\n'
def tile_inner(p, art, rx=28):
# inset the art so the window's lower corners clear the tile's rounding
return (f'<defs><clipPath id="{p}clip"><rect width="128" height="128" rx="{rx}"/></clipPath></defs>'
f'<g clip-path="url(#{p}clip)"><rect width="128" height="128" fill="{GROUND}"/>'
f'<g transform="translate(64 64) scale(.9) translate(-64 -60)">{art(p)}</g></g>')
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'<path transform="translate({cms_x:.2f} {cms_base:.2f}) scale({CMS_S})" d="{WORD["cms"]["d"]}"'
pill_rect = f'x="{PILL_X:.2f}" y="{MID - PILL_H / 2:.2f}" width="{PILL_W:.2f}" height="{PILL_H}" rx="13.3"'
if mono:
knock = (f'<mask id="{p}k" maskUnits="userSpaceOnUse" x="{PILL_X - 1:.2f}" y="{MID - PILL_H / 2 - 1:.2f}" width="{PILL_W + 2:.2f}" height="{PILL_H + 2}">'
f'<rect {pill_rect} fill="#fff"/>{cms} fill="#000"/></mask>')
pill_svg = f'<defs>{knock}</defs><rect {pill_rect} fill="{pill}" mask="url(#{p}k)"/>'
else:
pill_svg = f'<rect {pill_rect} fill="{pill}"/>{cms} fill="{ptxt}"/>'
hot = WORD["hot"]["d"]
return (f'<defs><clipPath id="{p}h"><path d="{hot}"/></clipPath></defs>'
f'<path d="{hot}" fill="none" stroke="{fg}" stroke-width="{HOLLOW}" clip-path="url(#{p}h)"/>'
f'<rect x="{SPLIT_X:.2f}" y="{MID - SPLIT_H / 2:.2f}" width="{SPLIT_W}" height="{SPLIT_H}" rx="2.5" fill="{split}"/>'
f'<path transform="translate({DOG_X:.2f} 0)" d="{WORD["dog"]["d"]}" fill="{fg}"/>'
+ 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'<svg x="0" y="{top:.2f}" width="{MARK_H}" height="{MARK_H}" viewBox="0 0 128 128">{tile_inner(p + "m", art)}</svg>'
f'<g transform="translate({MARK_H + gap} 0)">{word(p, theme)}</g>')
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'<svg x="{x_m:.2f}" y="0" width="{m}" height="{m}" viewBox="0 0 128 128">{tile_inner(p + "m", full)}</svg>'
f'<g transform="translate({(w - WORD_W) / 2:.2f} {y_w:.2f})">{word(p, theme)}</g>')
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'<path transform="translate({x:.2f} {y:.2f}) scale({size / 100:.4f})" d="{WORD[key]["d"]}" fill="{fill}"/>'
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'<rect x="{px}" y="{y0 + 236}" width="300" height="14" rx="7" fill="{PAGE_LINE}"/>')
dots = "".join(f'<circle cx="{x0 + 30 + i * 24}" cy="{y0 + bar / 2}" r="7" fill="{c}"/>'
for i, c in enumerate((KETCHUP, MUSTARD, "#7FBF6A")))
win = (f'<defs><pattern id="{p}g" width="20" height="20" patternUnits="userSpaceOnUse"><path d="M20 0 H0 V20" fill="none" stroke="{BP_GRID}" stroke-opacity=".25"/></pattern>'
f'<clipPath id="{p}w"><rect x="{x0}" y="{y0}" width="{x1 - x0}" height="400" rx="22"/></clipPath></defs>'
f'<g clip-path="url(#{p}w)"><rect x="{x0}" y="{y0}" width="{x1 - x0}" height="400" fill="{PAGE}"/>'
f'<rect x="{x0}" y="{y0}" width="{mid - x0}" height="400" fill="{BP_NAVY}"/><rect x="{x0}" y="{y0}" width="{mid - x0}" height="400" fill="url(#{p}g)"/>'
f'{src}{ren}'
f'<rect x="{mid - 4}" y="{y0 + bar}" width="8" height="400" fill="{MUSTARD}"/>'
f'<rect x="{x0}" y="{y0}" width="{x1 - x0}" height="{bar}" fill="{PAGE}"/></g>'
f'<path d="M{x0} {y0 + bar} H{x1}" stroke="{INK}" stroke-width="9"/>{dots}'
f'<circle cx="{mid}" cy="{y0 + 268}" r="15" fill="{MUSTARD}" stroke="{INK}" stroke-width="6"/>'
f'<rect x="{x0}" y="{y0}" width="{x1 - x0}" height="400" rx="22" fill="none" stroke="{INK}" stroke-width="12"/>')
# the dog with its paws on the window's top edge: small-cut geometry, toes back on at this size
pup = (f'<g transform="translate(1066 {y0}) scale(2.5) translate(-64 -90)">'
+ dog("translate(64 58) scale(1) translate(-64 -60)")
+ paws("translate(64 91) scale(1.04) translate(-64 -88)") + '</g>')
name = f'<g transform="translate(80 70) scale(.98) translate(0 {-WORD_TOP:.2f})">{word(p, "mustard")}</g>'
return svg(f"0 0 {W} {H}",
f'<rect width="{W}" height="{H}" fill="{MUSTARD}"/>{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'<defs><clipPath id="{p}c"><rect x="0" y="0" width="128" height="124"/></clipPath></defs>'
f'<g clip-path="url(#{p}c)">{small(p)}</g>', 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'<rect width="128" height="128" fill="{GROUND}"/>'
f'<g transform="translate(64 70) scale(.84) translate(-64 -70)">{small(p)}</g>', 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")