← All ivy-nodes
IVYXSTUDIO · IVY NODE
D

Design: logo concepts as SVG

ivy.node.design-logo-mark · v0.1.0

ivyx✓

Draws three monogram concepts (a mono mark, a badge, a wordmark) from a name, initials and a palette, as SVG. First concepts to react to, not a finished identity; no model, no network.

#design#logo#svg#agency

Inputs

FieldTypeDescription
specobjectname, initials, palette, tagline, as a model wrote them from the brief.
namestringNot declared
paletteobjectbg, accent, text as hex colours; a colour that is not a hex is replaced from a safe pair.

Outputs

FieldTypeDescription
filesrequiredarrayThe files made, each with its bytes as base64.
countrequiredintegerNot declared
initialsstringNot declared
paletteobjectbg, accent, text as hex colours; a colour that is not a hex is replaced from a safe pair.

Source

python

# Input preparation
inp = __ivy_ctx__["nodes"][__ivy_node_id__]["input"]

# Compute
# Shared by every design node (built into each by build.mjs): colours, text, files.
import base64, re, math

SAFE_PALETTES = [
    {"bg": "#1E1B4B", "accent": "#F6A23B", "text": "#F8F7FF"},
    {"bg": "#F4F2FF", "accent": "#6C3BFF", "text": "#1E1B4B"},
    {"bg": "#0F3D3E", "accent": "#E8C547", "text": "#F3F7F6"},
    {"bg": "#FFF4E6", "accent": "#D9480F", "text": "#2B1D12"},
    {"bg": "#101418", "accent": "#3DDC97", "text": "#F2F4F6"},
]

def hex_ok(s):
    return isinstance(s, str) and re.fullmatch(r"#[0-9a-fA-F]{6}", s.strip()) is not None

def palette_of(p, seed=0):
    """A palette with three valid hex colours; what the model got wrong is replaced from a safe pair."""
    base = SAFE_PALETTES[abs(int(seed)) % len(SAFE_PALETTES)]
    out = {}
    p = p if isinstance(p, dict) else {}
    for k in ("bg", "accent", "text"):
        v = p.get(k)
        out[k] = v.strip().upper() if hex_ok(v) else base[k]
    if out["bg"].upper() == out["text"].upper():
        out["text"] = base["text"] if base["text"].upper() != out["bg"].upper() else base["bg"]
    return out

def rgb(h):
    h = h.lstrip("#")
    return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))

def mix(a, b, t):
    return tuple(int(round(a[i] + (b[i] - a[i]) * t)) for i in range(3))

def to_hex(c):
    return "#%02X%02X%02X" % c

def lum(h):
    r, g, b = [x / 255 for x in rgb(h)]
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def esc(s):
    return (str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;"))

def text_of(v, limit, default=""):
    s = " ".join(str(v).split()) if isinstance(v, (str, int, float)) else ""
    if not s:
        s = default
    return s[:limit].rstrip() if len(s) > limit else s

def wrap(s, max_chars):
    words = s.split()
    lines, cur = [], ""
    for w in words:
        if cur and len(cur) + 1 + len(w) > max_chars:
            lines.append(cur)
            cur = w
        else:
            cur = (cur + " " + w).strip()
    if cur:
        lines.append(cur)
    return lines or [""]

def b64(data):
    return base64.b64encode(data).decode("ascii")

def file_entry(name, mime, data, kind, width=None, height=None, caption=None, variant=None, extra=None):
    f = {"name": name, "mime": mime, "kind": kind, "bytes": len(data), "data": b64(data)}
    if width:
        f["width"] = int(width)
    if height:
        f["height"] = int(height)
    if caption:
        f["caption"] = caption
    if variant:
        f["variant"] = variant
    if extra:
        f.update(extra)
    return f

def slug(s):
    s = re.sub(r"[^a-z0-9]+", "-", str(s).lower()).strip("-")
    return s[:40] or "design"

def font():
    return "Inter, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif"


class DesignLogoMark:
    "Design: logo concepts as SVG"

    def run(self, inp):
        out = {}
        # design-logo-mark: three monogram variants (mono, badge, wordmark) as SVG from a name and a palette.
        spec = inp.get("spec") if isinstance(inp.get("spec"), dict) else {}
        name = text_of(spec.get("name") or inp.get("name"), 40, "Brand")
        words = [w for w in name.replace("-", " ").split() if w]
        initials = text_of(spec.get("initials"), 3, "".join(w[0] for w in words[:2]).upper() or "B").upper()
        pal = palette_of(spec.get("palette") or inp.get("palette"), sum(ord(c) for c in name))
        tag = text_of(spec.get("tagline"), 40, "")
        FF = font()
        S = 512

        def mono():
            return """<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" role="img" aria-label="%s monogram">
<rect width="%d" height="%d" fill="%s"/>
<circle cx="%d" cy="%d" r="%d" fill="%s"/>
<text x="%d" y="%d" text-anchor="middle" font-family="%s" font-size="%d" font-weight="800" fill="%s">%s</text>
</svg>""" % (S, S, S, S, esc(name), S, S, pal["bg"], S // 2, S // 2, int(S * 0.36), pal["accent"], S // 2, int(S * 0.5 + S * 0.26 * 0.36), FF, int(S * 0.26 if len(initials) < 3 else S * 0.2), pal["bg"], esc(initials))

        def badge():
            r = S * 0.34
            pts = " ".join("%.1f,%.1f" % (S / 2 + r * __import__("math").cos(__import__("math").radians(60 * i - 30)), S * 0.42 + r * __import__("math").sin(__import__("math").radians(60 * i - 30))) for i in range(6))
            return """<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" role="img" aria-label="%s badge">
<rect width="%d" height="%d" fill="%s"/>
<polygon points="%s" fill="%s"/>
<polygon points="%s" fill="none" stroke="%s" stroke-width="%d" transform="translate(%d %d) scale(0.86) translate(%d %d)"/>
<text x="%d" y="%d" text-anchor="middle" font-family="%s" font-size="%d" font-weight="800" fill="%s">%s</text>
<text x="%d" y="%d" text-anchor="middle" font-family="%s" font-size="%d" font-weight="700" letter-spacing="4" fill="%s">%s</text>
</svg>""" % (S, S, S, S, esc(name), S, S, pal["bg"], pts, pal["accent"], pts, pal["bg"], 6, S // 2, int(S * 0.42), -S // 2, -int(S * 0.42), S // 2, int(S * 0.42 + S * 0.2 * 0.36), FF, int(S * 0.2 if len(initials) < 3 else S * 0.15), pal["bg"], esc(initials), S // 2, int(S * 0.88), FF, int(S * 0.055), pal["text"], esc(name.upper()))

        def wordmark():
            fs = int(min(S * 0.17, (S * 0.86) / max(len(name), 1) * 1.7))
            return """<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" role="img" aria-label="%s wordmark">
<rect width="%d" height="%d" fill="%s"/>
<text x="%d" y="%d" text-anchor="middle" font-family="%s" font-size="%d" font-weight="800" letter-spacing="-1" fill="%s">%s</text>
<circle cx="%d" cy="%d" r="%d" fill="%s"/>
<text x="%d" y="%d" text-anchor="middle" font-family="%s" font-size="%d" fill="%s" opacity="0.8">%s</text>
</svg>""" % (S, S // 2, S, S // 2, esc(name), S, S // 2, pal["bg"], S // 2, int(S * 0.25 + fs * 0.35), FF, fs, pal["text"], esc(name), int(S * 0.5 + len(name) * fs * 0.3 + fs * 0.25), int(S * 0.25 + fs * 0.3), int(fs * 0.12), pal["accent"], S // 2, int(S * 0.25 + fs * 0.35 + fs * 0.55), FF, int(fs * 0.3), pal["text"], esc(tag))

        files = [
            file_entry("%s-mono.svg" % slug(name), "image/svg+xml", mono().encode("utf-8"), "vector", S, S, "Monogram: %s." % initials, "mono"),
            file_entry("%s-badge.svg" % slug(name), "image/svg+xml", badge().encode("utf-8"), "vector", S, S, "Badge with the name.", "badge"),
            file_entry("%s-wordmark.svg" % slug(name), "image/svg+xml", wordmark().encode("utf-8"), "vector", S, S // 2, "Wordmark.", "wordmark"),
        ]
        out = {"files": files, "count": 3, "initials": initials, "palette": pal}
        return out


result = DesignLogoMark().run(inp)

# Output collection (the runner reads it here)
out = __ivy_ctx__["nodes"][__ivy_node_id__]["output"]
out.update(result)

Tests

Requires: python:3.9

  • bluefin

    A name and initials make three concepts, the mono mark first and the wordmark half as tall.

  • name-only

    A name with no initials takes its first letter.

  • defaults

    No name still draws the three concepts, for a brand called brand.