← All ivy-nodes
IVYXSTUDIO · IVY NODE
D
Design: a key visual as PNG
ivy.node.design-png-art · v0.1.0
ivyx✓
Paints an abstract key visual (gradient, waves, grid or blobs) in a palette and writes it as a PNG with the standard library, no Pillow. The picture behind a card or a post; deterministic for a seed.
#design#png#visual#agency
Inputs
| Field | Type | Description |
|---|---|---|
| palette | object | bg, accent, text as hex colours; a colour that is not a hex is replaced from a safe pair. |
| style | string | gradient when omitted. |
| size | string | WxH up to 1600 a side; 1200x630 when omitted. |
| seed | integer | The same seed paints the same picture. |
| caption | string | Not declared |
| name_prefix | string | Not declared |
Outputs
| Field | Type | Description |
|---|---|---|
| filesrequired | array | The files made, each with its bytes as base64. |
| countrequired | integer | Not declared |
| palette | object | bg, accent, text as hex colours; a colour that is not a hex is replaced from a safe pair. |
| style | string | Not declared |
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("&", "&").replace("<", "<").replace(">", ">").replace('"', """))
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"
# design-png-art: a raster key visual (gradient, waves, grid, blobs) written as PNG with the standard library.
import zlib, struct, math, random
class DesignPngArt:
"Design: a key visual as PNG"
def run(self, inp):
out = {}
pal = palette_of(inp.get("palette"), inp.get("seed") or 0)
style = inp.get("style") if inp.get("style") in ("gradient", "waves", "grid", "blobs") else "gradient"
try:
w, h = [int(x) for x in str(inp.get("size") or "1200x630").lower().split("x")]
except Exception:
w, h = 1200, 630
w, h = max(64, min(w, 1600)), max(64, min(h, 1600))
seed = int(inp.get("seed") or 7)
rnd = random.Random(seed)
bg, ac, tx = rgb(pal["bg"]), rgb(pal["accent"]), rgb(pal["text"])
def clamp(v):
return 0 if v < 0 else 255 if v > 255 else int(v)
rows = []
if style == "gradient":
hx, hy, hr = rnd.uniform(0.55, 0.85), rnd.uniform(0.2, 0.5), rnd.uniform(0.35, 0.55)
for y in range(h):
row = bytearray()
fy = y / h
for x in range(w):
fx = x / w
t = (fx * 0.6 + fy * 0.4)
c = mix(bg, ac, t * 0.85)
d = math.hypot(fx - hx, (fy - hy) * h / w)
if d < hr:
k = (1 - d / hr) ** 2 * 0.55
c = mix(c, tx, k)
row += bytes(c)
rows.append(bytes(row))
elif style == "waves":
bands = 5 + rnd.randint(0, 3)
phase = [rnd.uniform(0, 6.28) for _ in range(bands)]
for y in range(h):
row = bytearray()
fy = y / h
for x in range(w):
fx = x / w
v = 0.0
for i in range(bands):
v += math.sin(fx * (3 + i * 1.3) + fy * (4 - i * 0.5) + phase[i]) / bands
t = (v + 1) / 2
c = mix(bg, ac, t) if t < 0.72 else mix(ac, tx, (t - 0.72) * 2.2)
row += bytes(c)
rows.append(bytes(row))
elif style == "grid":
step = max(24, int(min(w, h) / 14))
r = step * 0.18
for y in range(h):
row = bytearray()
gy = (y % step) - step / 2
for x in range(w):
gx = (x % step) - step / 2
d = math.hypot(gx, gy)
fade = 0.35 + 0.65 * (x / w)
if d < r * fade:
c = ac if ((x // step + y // step) % 7) else tx
else:
c = mix(bg, ac, 0.08 * (y / h))
row += bytes(c)
rows.append(bytes(row))
else: # blobs
blobs = [(rnd.uniform(0.1, 0.9), rnd.uniform(0.1, 0.9), rnd.uniform(0.18, 0.4), ac if i % 3 else tx) for i in range(5)]
for y in range(h):
row = bytearray()
fy = y / h
for x in range(w):
fx = x / w
c = bg
for bx, by, br, bc in blobs:
d = math.hypot((fx - bx) * w / h, fy - by)
if d < br:
k = (1 - d / br)
k = k * k * (3 - 2 * k) * 0.85
c = mix(c, bc, k)
row += bytes(c)
rows.append(bytes(row))
def chunk(t, d):
return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
raw = b"".join(b"\x00" + r for r in rows)
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw, 7)) + chunk(b"IEND", b"")
name = "%s-%s-%dx%d.png" % (slug(inp.get("name_prefix") or "visual"), style, w, h)
out = {"files": [file_entry(name, "image/png", png, "image", w, h, text_of(inp.get("caption"), 160, "Key visual, %s style." % style), style)], "count": 1, "palette": pal, "style": style}
return out
result = DesignPngArt().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
- waves
A waves visual of 120 by 80 is one PNG of that size in the palette.
- defaults
No input paints the gradient at 1200 by 630.
- clamped
A size over 1600 a side is held to 1600, and an unknown style is the gradient.