Files
time/ventures/slyng/tools/brand/direction.py
T

211 lines
8.7 KiB
Python

"""Chosen direction: the concept-5 app icon in concept 8's palette (ink, white, volt).
The icon is drawn in a 1024 box (y up). `icon_svg(state)` renders any moment of
the pull-and-release, so the static icon, the storyboard and the animation all
come from one geometry."""
import math
from kit import INK, WHITE, VOLT, BONE, Mark
from concepts import squircle, LEX, UNB, c1, c8
from sling import make_y, f
S = 1024
CX = S / 2
H_FRAME = S * 0.70
BASE = S * 0.14
R_STONE = S * 0.065
BAND_W = S * 0.045
Y = make_y(CX, H_FRAME, S * 0.15, S * 0.14, S * 0.30, H_FRAME * 0.40, base=0,
r_u=S * 0.075, r_o=S * 0.04, tips="round", bottom="round")
_L, _R = Y.anchors()
L = (_L[0], _L[1] + BASE)
R = (_R[0], _R[1] + BASE)
UB = Y.geometry()["u_bottom"] + BASE # top of the U, icon coords
P_REST = (CX, UB + R_STONE + S * 0.02) # pouch when loaded
P_DRAWN = (CX, UB - S * 0.17) # pouch at full draw
TIP_Y = L[1] # straight band height
PALETTES = {
"zap": dict(ground=INK, frame=WHITE, band=VOLT, stone=VOLT, knock=INK),
"volt": dict(ground=VOLT, frame=INK, band=INK, stone=INK, knock=VOLT),
}
def icon_svg(pouch=None, stone=None, stone_scale=1.0, palette="zap", knock=True,
band_behind=False, size=None, label="SLYNG app icon", extra_stone=None):
"""pouch: band apex (None = loaded rest). stone: stone centre (None = in the
pouch; False = no stone). extra_stone: (x, y, scale) for a second stone
(the reload)."""
c = PALETTES[palette]
pouch = P_REST if pouch is None else pouch
if stone is None:
stone = pouch
kw = S * 0.016 if knock else 0
sq = squircle(S / 2, S / 2, S / 2)
frame = f'<path transform="translate(0 {f(BASE)})" d="{Y.path()}" fill="{c["frame"]}"/>'
d = f"M{f(L[0])} {f(L[1])} L{f(pouch[0])} {f(pouch[1])} L{f(R[0])} {f(R[1])}"
band = ""
if knock and not band_behind:
band += (f'<path d="{d}" fill="none" stroke="{c["knock"]}" stroke-width="{f(BAND_W + 2 * kw)}" '
f'stroke-linejoin="round" stroke-linecap="round"/>')
band += (f'<path d="{d}" fill="none" stroke="{c["band"]}" stroke-width="{f(BAND_W)}" '
f'stroke-linejoin="round" stroke-linecap="round"/>')
def stone_el(x, y, sc):
r = R_STONE * sc
s = ""
if knock:
s += f'<circle cx="{f(x)}" cy="{f(y)}" r="{f(r + kw)}" fill="{c["knock"]}"/>'
return s + f'<circle cx="{f(x)}" cy="{f(y)}" r="{f(r)}" fill="{c["stone"]}"/>'
stones = ""
if stone is not False:
stones += stone_el(stone[0], stone[1], stone_scale)
if extra_stone:
stones += stone_el(*extra_stone)
art = (band + frame) if band_behind else (frame + band)
body = (f'<defs><clipPath id="sq"><path d="{sq}"/></clipPath></defs>'
f'<path d="{sq}" fill="{c["ground"]}"/>'
f'<g clip-path="url(#sq)">{art}{stones}</g>')
size_attr = f' width="{size}" height="{size}"' if size else ""
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 {-S} {S} {S}"{size_attr} role="img" '
f'aria-label="{label}"><g transform="scale(1 -1)">{body}</g></svg>')
def icon_mark_z(palette="zap", knock=True, band_behind=False):
"""The static icon as a Mark (for lockups)."""
svg = icon_svg(palette=palette, knock=knock, band_behind=band_behind)
inner = svg[svg.index('<g transform="scale(1 -1)">') + len('<g transform="scale(1 -1)">'):-len("</g></svg>")]
# clip ids must be unique per document; rename for embedding
inner = inner.replace('id="sq"', f'id="sq-{palette}"').replace("url(#sq)", f"url(#sq-{palette})")
mk = Mark()
mk.add(inner, (0, 0, S, S))
return mk
# ---------------------------------------------------------------- wordmarks for lockups
from kit import word, ypath, y_box, band as band_fn, dot as dot_fn, glyph
WORDMARKS = {
# face, outer half-width of the slingshot Y, kerning, handle/arm weights, crotch height, fillets
"lexend": dict(face=LEX, outer=396, kern={"LY": -4, "YN": 22}, w_k=0.93, wa_k=0.82, yj=0.30, r_u=70, r_o=34,
bw=44, r_dot=64, lift=20),
"unbounded": dict(face=UNB, outer=470, kern={"LY": -40, "YN": 14}, w_k=0.92, wa_k=0.82, yj=0.30, r_u=90,
r_o=40, bw=46, r_dot=70, lift=30),
}
def wordmark_z(kind="lexend", mode="full", ink=WHITE, band_c=VOLT, knock=INK, kw=16):
"""mode: full = slingshot Y with band and stone, frame = slingshot Y only,
plain = the typeface's own Y."""
p = WORDMARKS[kind]
face = p["face"]
H, stem = face.cap, face.stem("L")[0]
mk = Mark()
if mode == "plain":
word(mk, face, "SLYNG", ink, kern={"LY": -40 if kind == "unbounded" else -30})
return mk
outer = p["outer"]
pos, _ = word(mk, face, "SLYNG", ink, slot="Y", slot_adv=2 * outer + 34, kern=p["kern"])
cx = pos[2][1] + outer + 17
Yw = make_y(cx, H, stem * p["w_k"], stem * p["wa_k"], outer, H * p["yj"], r_u=p["r_u"], r_o=p["r_o"],
tips="round")
mk.add(ypath(Yw, ink), y_box(Yw))
if mode == "full":
Lw, Rw = Yw.anchors()
ub = Yw.geometry()["u_bottom"]
Pt = (cx, ub + p["r_dot"] + p["lift"])
mk.add(band_fn(Lw, Pt, Rw, p["bw"], band_c, knock=knock, kw=kw))
mk.add(dot_fn(Pt, p["r_dot"], band_c, knock=knock, kw=kw))
return mk
def lockup_z(kind="lexend", mode="full", palette="zap", ink=WHITE, band_c=VOLT, knock=INK):
wm = wordmark_z(kind, mode, ink, band_c, knock)
H = WORDMARKS[kind]["face"].cap
ic = icon_mark_z(palette)
s = 1.3 * H / S
gap = 0.33 * H
x0 = wm.box[0] - (S * s + gap)
y0 = H / 2 - (S / 2) * s
mk = Mark()
mk.add(f'<g transform="translate({f(x0)} {f(y0)}) scale({s:.5f})">{"".join(ic.parts)}</g>',
(x0, y0, x0 + S * s, y0 + S * s))
mk.parts += wm.parts
mk.grow(*wm.box)
return mk
def mark_bare(palette="zap"):
"""The slingshot without its squircle (what the icon reads as on an ink ground)."""
c = PALETTES[palette]
kw = S * 0.016
frame = f'<path transform="translate(0 {f(BASE)})" d="{Y.path()}" fill="{c["frame"]}"/>'
P = P_REST
dd = f"M{f(L[0])} {f(L[1])} L{f(P[0])} {f(P[1])} L{f(R[0])} {f(R[1])}"
band = (f'<path d="{dd}" fill="none" stroke="{c["knock"]}" stroke-width="{f(BAND_W + 2 * kw)}" '
f'stroke-linejoin="round" stroke-linecap="round"/>'
f'<path d="{dd}" fill="none" stroke="{c["band"]}" stroke-width="{f(BAND_W)}" '
f'stroke-linejoin="round" stroke-linecap="round"/>')
stone = (f'<circle cx="{f(P[0])}" cy="{f(P[1])}" r="{f(R_STONE + kw)}" fill="{c["knock"]}"/>'
f'<circle cx="{f(P[0])}" cy="{f(P[1])}" r="{f(R_STONE)}" fill="{c["stone"]}"/>')
mk = Mark()
half = S * 0.30
mk.add(frame + band + stone, (CX - half, BASE, CX + half, BASE + H_FRAME))
return mk
def lockup_dark(kind="lexend"):
"""Ink-ground lockup: the bare slingshot sized to the wordmark, white type."""
wm = wordmark_z(kind, "frame", ink=WHITE)
H = WORDMARKS[kind]["face"].cap
bm = mark_bare()
s = 1.24 * H / H_FRAME
gap = 0.42 * H
bx0, by0, bx1, by1 = bm.box
x_right = wm.box[0] - gap
tx = x_right - bx1 * s
ty = (H / 2) - ((by0 + by1) / 2) * s
mk = Mark()
mk.add(f'<g transform="translate({f(tx)} {f(ty)}) scale({s:.5f})">{"".join(bm.parts)}</g>',
(tx + bx0 * s, ty + by0 * s, tx + bx1 * s, ty + by1 * s))
mk.parts += wm.parts
mk.grow(*wm.box)
return mk
def icon_inner(palette="zap", uid="i", **state):
"""The icon's drawing (y up, 0..S) for nesting inside another mark."""
svg = icon_svg(palette=palette, **state)
head = '<g transform="scale(1 -1)">'
inner = svg[svg.index(head) + len(head):-len("</g></svg>")]
return inner.replace('id="sq"', f'id="sq-{uid}"').replace("url(#sq)", f"url(#sq-{uid})")
GAPS = {"unbounded": (0.095, 0.115), "lexend": (0.12, 0.15)}
def wordmark_icon_y(kind="unbounded", tone="light", k=1.08, state=None, uid=None):
"""SLYNG where the Y is the app icon itself. Light: ink letters with the
ink icon; dark: white letters with the volt icon. `state` animates the icon."""
face = WORDMARKS[kind]["face"]
H = face.cap
ink = INK if tone == "light" else WHITE
palette = "zap" if tone == "light" else "volt"
size = H * k
gl, gr = GAPS[kind]
mk = Mark()
pos, x = word(mk, face, "SL", ink)
lb = face.bounds("L")
x_icon = pos[1][1] + lb[2] + H * gl
y_icon = H / 2 - size / 2
s = size / S
inner = icon_inner(palette, uid or f"{kind}-{tone}", **(state or {}))
mk.add(f'<g transform="translate({f(x_icon)} {f(y_icon)}) scale({s:.5f})">{inner}</g>',
(x_icon, y_icon, x_icon + size, y_icon + size))
nb = face.bounds("N")
x_n = x_icon + size + H * gr - nb[0]
word(mk, face, "NG", ink, x0=x_n)
return mk