diff --git a/ventures/househand/drawon.py b/ventures/househand/drawon.py new file mode 100644 index 0000000..103e207 --- /dev/null +++ b/ventures/househand/drawon.py @@ -0,0 +1,553 @@ +"""Make a drawing draw itself: an animated SVG (SMIL, no script) that replays how the sheet is built. + + python3 drawon.py IN.svg OUT.svg [--budget 12] [--preview OUT.mp4] + +Four passes, the way an illustrator works: + 1. inks every ink line is traced with stroke-dashoffset by one pen. A tube (an ink stroke with its colour core on + the same path) grows as an outlined noodle; a closed shape fills with paper the moment its outline closes, + so from then on it hides what is behind it. Labels and solid ink marks appear in their turn. + 2. flats each paper fill and each tube core floods to its colour, in the same order. + 3. shading the saturated shadows (class "s", and the clip groups that hold them) fade in. + 4. fx strokeless matter (class "fx": blush, inner ears, sparkles, confetti) appears last. + +Order: document order, except inside a character drawn by house.figure() with HOUSE_TAGS=1, whose parts are drawn the +way an artist would: head, hair, ears, torso, arms, tail, skirt, legs, shoes. The last frame is the original drawing. +How long the line work takes grows with the square root of the number of strokes (--budget caps it, in seconds). + +No browser needed to build: path lengths are measured here (lines, cubic/quadratic Béziers, arcs, transforms). +--preview renders an MP4 (and a GIF next to it) by seeking the SMIL clock in Chromium; that part needs Playwright. +Import it to use from Python: animate(svg_text) -> (animated_svg_text, seconds). +""" +import argparse +import math +import os +import re +import xml.etree.ElementTree as ET + +SVG = "http://www.w3.org/2000/svg" +ET.register_namespace("", SVG) +PAPER, INK = "#FFEAD2", "#2B2135" +SKIP = {"style", "defs", "clipPath", "title", "desc", "metadata", "linearGradient", "radialGradient", "mask", + "pattern", "symbol", "filter", "animate", "set"} +LEAVES = {"path", "circle", "ellipse", "rect", "line", "polyline", "polygon", "text"} +PAINT = ("fill", "stroke", "stroke-width", "stroke-dasharray", "opacity", "fill-opacity", "stroke-linecap", + "stroke-linejoin", "stroke-opacity") +# parts from house.figure(HOUSE_TAGS=1), in the order an artist draws a character: head first, outward from there +PART_RANK = {"head": 0, "over": 1, "under": 2, "accessory": 3, "collar": 4, "torso": 5, "arm-near": 6, "arm": 6, + "arm-far": 7, "mid": 8, "skirt": 9, "legs": 10, "shoes": 11, "ground": 12} + + +def tag(el): + return el.tag.split("}", 1)[-1] + + +# ---------- geometry: sample every element as polylines, in its own coordinates ---------- +NUM = r"[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?" +TOK = re.compile(r"[MmLlHhVvCcSsQqTtAaZz]|" + NUM) + + +def _arc(x1, y1, rx, ry, phi, fa, fs, x2, y2): + """SVG endpoint arc -> points (implementation notes F.6.5)""" + if rx == 0 or ry == 0 or (x1 == x2 and y1 == y2): + return [(x2, y2)] + rx, ry = abs(rx), abs(ry) + c, s = math.cos(math.radians(phi)), math.sin(math.radians(phi)) + dx, dy = (x1 - x2) / 2, (y1 - y2) / 2 + x1p, y1p = c * dx + s * dy, -s * dx + c * dy + lam = x1p ** 2 / rx ** 2 + y1p ** 2 / ry ** 2 + if lam > 1: + rx, ry = rx * math.sqrt(lam), ry * math.sqrt(lam) + num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p + den = rx * rx * y1p * y1p + ry * ry * x1p * x1p + k = math.sqrt(max(0.0, num / den)) if den else 0.0 + if fa == fs: + k = -k + cxp, cyp = k * rx * y1p / ry, -k * ry * x1p / rx + cx, cy = c * cxp - s * cyp + (x1 + x2) / 2, s * cxp + c * cyp + (y1 + y2) / 2 + + def ang(ux, uy, vx, vy): + a = math.atan2(ux * vy - uy * vx, ux * vx + uy * vy) + return a + t1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry) + dt = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry) + if not fs and dt > 0: + dt -= 2 * math.pi + elif fs and dt < 0: + dt += 2 * math.pi + n = max(4, int(abs(dt) / (math.pi / 24))) + pts = [] + for i in range(1, n + 1): + t = t1 + dt * i / n + pts.append((cx + rx * math.cos(t) * c - ry * math.sin(t) * s, cy + rx * math.cos(t) * s + ry * math.sin(t) * c)) + return pts + + +def _bez(p, steps): + out = [] + for i in range(1, steps + 1): + t = i / steps + u = 1 - t + if len(p) == 4: + x = u ** 3 * p[0][0] + 3 * u * u * t * p[1][0] + 3 * u * t * t * p[2][0] + t ** 3 * p[3][0] + y = u ** 3 * p[0][1] + 3 * u * u * t * p[1][1] + 3 * u * t * t * p[2][1] + t ** 3 * p[3][1] + else: + x = u * u * p[0][0] + 2 * u * t * p[1][0] + t * t * p[2][0] + y = u * u * p[0][1] + 2 * u * t * p[1][1] + t * t * p[2][1] + out.append((x, y)) + return out + + +def path_polylines(d): + toks = TOK.findall(d or "") + polys, cur = [], None + x = y = sx = sy = 0.0 + lc = None # last control point, for S/T + cmd, i = None, 0 + + def nums(k): + nonlocal i + v = [float(t) for t in toks[i:i + k]] + i += k + return v + while i < len(toks): + if toks[i].isalpha(): + cmd = toks[i] + i += 1 + if cmd in "Zz": + if cur: + cur.append((sx, sy)) + x, y, lc = sx, sy, None + continue + elif cmd is None: + break + rel = cmd.islower() + C = cmd.upper() + ox, oy = (x, y) if rel else (0.0, 0.0) + if C == "M": + a, b = nums(2) + x, y = ox + a, oy + b + sx, sy = x, y + cur = [(x, y)] + polys.append(cur) + cmd = "l" if rel else "L" # implicit lineto after a moveto + lc = None + continue + if cur is None: + cur = [(x, y)] + polys.append(cur) + if C == "L": + a, b = nums(2) + x, y = ox + a, oy + b + cur.append((x, y)) + lc = None + elif C == "H": + (a,) = nums(1) + x = ox + a + cur.append((x, y)) + lc = None + elif C == "V": + (b,) = nums(1) + y = oy + b + cur.append((x, y)) + lc = None + elif C in "CS": + if C == "C": + v = nums(6) + c1 = (ox + v[0], oy + v[1]) + c2, e = (ox + v[2], oy + v[3]), (ox + v[4], oy + v[5]) + else: + v = nums(4) + c1 = (2 * x - lc[0], 2 * y - lc[1]) if lc else (x, y) + c2, e = (ox + v[0], oy + v[1]), (ox + v[2], oy + v[3]) + chord = math.dist((x, y), e) + math.dist((x, y), c1) + math.dist(c1, c2) + math.dist(c2, e) + cur.extend(_bez([(x, y), c1, c2, e], max(6, min(64, int(chord / 6))))) + lc = c2 + x, y = e + elif C in "QT": + if C == "Q": + v = nums(4) + c1, e = (ox + v[0], oy + v[1]), (ox + v[2], oy + v[3]) + else: + v = nums(2) + c1 = (2 * x - lc[0], 2 * y - lc[1]) if lc else (x, y) + e = (ox + v[0], oy + v[1]) + chord = math.dist((x, y), c1) + math.dist(c1, e) + cur.extend(_bez([(x, y), c1, e], max(6, min(64, int(chord / 6))))) + lc = c1 + x, y = e + elif C == "A": + v = nums(7) + e = (ox + v[5], oy + v[6]) + cur.extend(_arc(x, y, v[0], v[1], v[2], int(v[3]), int(v[4]), e[0], e[1])) + x, y = e + lc = None + else: + i += 1 + return polys + + +def ellipse_pts(cx, cy, rx, ry, n=96): + return [[(cx + rx * math.cos(2 * math.pi * k / n), cy + ry * math.sin(2 * math.pi * k / n)) for k in range(n + 1)]] + + +def geometry(el): + t = tag(el) + f = lambda k, dv=0.0: float(re.match(NUM, el.get(k, str(dv))).group(0)) if el.get(k) else dv + if t == "path": + return path_polylines(el.get("d")) + if t == "circle": + return ellipse_pts(f("cx"), f("cy"), f("r"), f("r")) + if t == "ellipse": + return ellipse_pts(f("cx"), f("cy"), f("rx"), f("ry")) + if t == "rect": + x, y, w, h = f("x"), f("y"), f("width"), f("height") + return [[(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)]] + if t == "line": + return [[(f("x1"), f("y1")), (f("x2"), f("y2"))]] + if t in ("polyline", "polygon"): + v = [float(n) for n in re.findall(NUM, el.get("points", ""))] + p = list(zip(v[0::2], v[1::2])) + return [p + ([p[0]] if t == "polygon" and p else [])] + return [] + + +# ---------- transforms ---------- +def mat_mul(a, b): + return (a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1], a[0] * b[2] + a[2] * b[3], + a[1] * b[2] + a[3] * b[3], a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5]) + + +def parse_transform(s): + m = (1, 0, 0, 1, 0, 0) + for name, args in re.findall(r"(\w+)\s*\(([^)]*)\)", s or ""): + v = [float(n) for n in re.findall(NUM, args)] + if name == "translate": + t = (1, 0, 0, 1, v[0], v[1] if len(v) > 1 else 0) + elif name == "scale": + t = (v[0], 0, 0, v[1] if len(v) > 1 else v[0], 0, 0) + elif name == "rotate": + a = math.radians(v[0]) + r = (math.cos(a), math.sin(a), -math.sin(a), math.cos(a), 0, 0) + if len(v) == 3: + t = mat_mul(mat_mul((1, 0, 0, 1, v[1], v[2]), r), (1, 0, 0, 1, -v[1], -v[2])) + else: + t = r + elif name == "matrix": + t = tuple(v[:6]) + elif name == "skewX": + t = (1, 0, math.tan(math.radians(v[0])), 1, 0, 0) + elif name == "skewY": + t = (1, math.tan(math.radians(v[0])), 0, 1, 0, 0) + else: + continue + m = mat_mul(m, t) + return m + + +def apply(m, p): + return (m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]) + + +# ---------- styles: inline the few class rules the early katas use, so SMIL can override everything ---------- +def flatten_styles(root): + rules = {} + for st in list(root.iter(f"{{{SVG}}}style")): + for sel, body in re.findall(r"([^{}]+)\{([^}]*)\}", st.text or ""): + props = dict((k.strip(), v.strip()) for k, v in (kv.split(":", 1) for kv in body.split(";") if ":" in kv)) + for s in sel.split(","): + s = s.strip() + if re.fullmatch(r"\.[\w-]+", s): + rules.setdefault(s[1:], {}).update(props) + for parent in list(root.iter()): + for st in [c for c in parent if tag(c) == "style"]: + parent.remove(st) + for el in root.iter(): + eff = {} + for c in el.get("class", "").split(): + eff.update(rules.get(c, {})) + if el.get("style"): + eff.update(dict((k.strip(), v.strip()) for k, v in (kv.split(":", 1) for kv in el.get("style").split(";") if ":" in kv))) + del el.attrib["style"] + for k, v in eff.items(): + el.set(k, v) # class rules and inline style both beat presentation attributes + + +# ---------- the animation ---------- +class Leaf: + __slots__ = ("el", "parent", "idx", "kind", "local_len", "len", "bbox", "fig", "part", "paint") + + +def classify(el, paint): + t = tag(el) + cls = el.get("class", "").split() + fill, stroke = paint.get("fill", "#000000"), paint.get("stroke", "none") + if t == "text": + return "text" + if "s" in cls: + return "shade" + if "fx" in cls: + return "fx" + has_stroke = stroke not in ("none", "") and float(paint.get("stroke-width", "1") or 1) > 0 + has_fill = fill not in ("none", "") + if has_stroke and paint.get("stroke-dasharray", "none") not in ("none", ""): + return "fadeline" # a dashed construction line: it appears, it isn't drawn + if has_stroke: + return "shape" if has_fill else "stroke" + if has_fill: + return "inkfill" if fill.upper() == INK else "flat" + return None + + +def walk(root): + """leaves in document order, with inherited paint, the figure/part they belong to and their transform""" + leaves, groups = [], [] + + def rec(el, inh, m, fig, part): + for ch in list(el): + t = tag(ch) + if t in SKIP or not isinstance(ch.tag, str): + continue + paint = dict(inh) + for k in PAINT: + if ch.get(k) is not None and k not in ("opacity",): + paint[k] = ch.get(k) + m2 = mat_mul(m, parse_transform(ch.get("transform"))) if ch.get("transform") else m + if t == "g": + f2 = ch.get("data-figure") and (ch, len(leaves)) or fig + p2 = ch.get("data-part") or part + if ch.get("clip-path"): + groups.append((ch, el, len(leaves))) + rec(ch, paint, m2, f2, p2) + continue + if t not in LEAVES: + continue + lf = Leaf() + lf.el, lf.parent, lf.idx, lf.fig, lf.part, lf.paint = ch, el, len(leaves), fig, part, paint + lf.kind = classify(ch, paint) + polys = geometry(ch) + loc = can = 0.0 + xs, ys = [], [] + for poly in polys: + tp = [apply(m2, p) for p in poly] + for a, b in zip(poly, poly[1:]): + loc += math.dist(a, b) + for a, b in zip(tp, tp[1:]): + can += math.dist(a, b) + xs += [p[0] for p in tp] + ys += [p[1] for p in tp] + lf.local_len, lf.len = loc, can + lf.bbox = (min(xs), min(ys), max(xs), max(ys)) if xs else (0, 0, 0, 0) + leaves.append(lf) + rec(root, {}, (1, 0, 0, 1, 0, 0), None, None) + return leaves, groups + + +def anim(el, attr, frm, to, begin, dur, spline=None): + a = ET.SubElement(el, f"{{{SVG}}}animate", attributeName=attr, values=f"{frm};{to}", begin=f"{begin:.3f}s", + dur=f"{max(dur, 0.01):.3f}s", fill="freeze") + if spline: + a.set("calcMode", "spline") + a.set("keyTimes", "0;1") + a.set("keySplines", spline) + + +def fmt(v): + return f"{v:.2f}".rstrip("0").rstrip(".") + + +def animate_tree(root, budget=12.0): + flatten_styles(root) + W = float(re.match(NUM, root.get("width", "0")).group(0)) if root.get("width") else 0 + H = float(re.match(NUM, root.get("height", "0")).group(0)) if root.get("height") else 0 + if (not W or not H) and root.get("viewBox"): + W, H = [float(n) for n in re.findall(NUM, root.get("viewBox"))[2:4]] + area = max(W * H, 1) + leaves, groups = walk(root) + + # clip groups that only hold shadows fade in as one piece in the shading pass + shade_groups = [] + for g, parent, first in groups: + inner = [lf for lf in leaves if _inside(lf.el, g)] + if inner and all(lf.kind == "shade" for lf in inner): + shade_groups.append((g, first, inner)) + for lf in inner: + lf.kind = None # handled by the group + # big fill-only shapes are the ground the drawing is made on (a sky, a dark panel): they stay put + for lf in leaves: + if lf.kind == "flat": + x0, y0, x1, y1 = lf.bbox + if (x1 - x0) * (y1 - y0) > 0.3 * area: + lf.kind = None + if lf.kind and tag(lf.el) == "rect" and lf.idx == 0: + lf.kind = None # the background sheet + + def key(lf): + if lf.fig is not None: + return (lf.fig[1], PART_RANK.get(lf.part, 13), lf.idx) + return (lf.idx, 0, lf.idx) + + # units: a tube's ink and its core(s) share one path in one parent, so they are traced as one stroke + units, by_geo = [], {} + for lf in leaves: + if lf.kind in ("shape", "stroke"): + gk = (id(lf.parent), tag(lf.el), lf.el.get("d"), lf.el.get("cx"), lf.el.get("cy"), lf.el.get("r"), + lf.el.get("rx"), lf.el.get("ry"), lf.el.get("points"), lf.el.get("x1"), lf.el.get("y1")) + if gk in by_geo: + by_geo[gk].append(lf) + continue + by_geo[gk] = [lf] + units.append(by_geo[gk]) + elif lf.kind in ("inkfill", "text", "fadeline"): + units.append([lf]) + units.sort(key=lambda u: min(key(lf) for lf in u)) + + # time budget: the line work takes ~1.1·√(strokes) seconds (capped), at one steady pen speed + n = max(len(units), 1) + B = min(max(1.1 * math.sqrt(n), 3.0), budget) + gap = min(0.04, 0.25 * B / n) + dmin = min(0.07, 0.45 * B / n) + fixed = lambda u: u[0].kind in ("inkfill", "text", "fadeline") + lens = [max(lf.len for lf in u) for u in units] + + def total(v): + return sum(0.9 * (min(0.12, dmin * 2) if fixed(u) else min(max(L / v, dmin), 0.5)) + gap for u, L in zip(units, lens)) + lo, hi = 1500.0, 1e7 + if total(lo) > B: + for _ in range(60): + mid = math.sqrt(lo * hi) + lo, hi = (mid, hi) if total(mid) > B else (lo, mid) + speed = hi + else: + speed = lo + + t = 0.3 + flood = [] # (key, element, attribute, colour) + for u, L in zip(units, lens): + lead = u[0] + if fixed(u): + dur = min(0.12, dmin * 2) + for lf in u: + op = lf.el.get("opacity", "1") + lf.el.set("opacity", "0") + anim(lf.el, "opacity", 0, op, t, dur) + else: + dur = min(max(L / speed, dmin), 0.5) + has_ink = any(lf.paint.get("stroke", "").upper() == INK for lf in u) + for lf in u: + e = lf.el + D = 1.02 * lf.local_len + 2 + e.set("stroke-dasharray", f"{fmt(D)} {fmt(2 * D + 4)}") + e.set("stroke-dashoffset", fmt(D + 1)) + anim(e, "stroke-dashoffset", fmt(D + 1), 0, t, dur, "0.45 0 0.55 1") + if lf.kind == "shape": # paper fill the moment the outline closes, colour in the flats pass + col, fo = lf.paint.get("fill"), e.get("fill-opacity", "1") + e.set("fill", PAPER) + e.set("fill-opacity", "0") + anim(e, "fill-opacity", 0, fo, t + dur, 0.06) + if col.upper() != PAPER: + flood.append((key(lf), e, "fill", col)) + elif has_ink and lf.paint.get("stroke", "").upper() != INK: # a tube's core: paper until the flats + col = lf.paint.get("stroke") + e.set("stroke", PAPER) + if col.upper() != PAPER: + flood.append((key(lf), e, "stroke", col)) + t += dur * 0.9 + gap + t_inks = t + 0.3 + + # flats: flood paper fills and tube cores to colour, and let the flat shapes in, in drawing order + flats = sorted([(k, e, a, c) for k, e, a, c in flood] + + [(key(lf), lf.el, "opacity", None) for lf in leaves if lf.kind == "flat"], key=lambda x: x[0]) + step = min(0.045, 1.8 / max(len(flats), 1)) + t = t_inks + for k, e, a, c in flats: + if a == "opacity": + op = e.get("opacity", "1") + e.set("opacity", "0") + anim(e, "opacity", 0, op, t, 0.22) + else: + anim(e, a, PAPER, c, t, 0.22) + t += step + t_flats = t + (0.25 if flats else 0) + + def fade(items, t, step, dur): + for e in items: + op = e.get("opacity", "1") + e.set("opacity", "0") + anim(e, "opacity", 0, op, t, dur) + t += step + return t + shade = sorted([(key(lf), lf.el) for lf in leaves if lf.kind == "shade"] + + [((first, 0, first), g) for g, first, inner in shade_groups], key=lambda x: x[0]) + shade_key = {} + for g, first, inner in shade_groups: # a clip group sits where its first shadow sits + shade_key[id(g)] = min(key(lf) for lf in inner) + shade = sorted(shade, key=lambda x: shade_key.get(id(x[1]), x[0])) + t_shade = fade([e for k, e in shade], t_flats, min(0.06, 0.9 / max(len(shade), 1)), 0.3) + (0.2 if shade else 0) + fx = sorted([(key(lf), lf.el) for lf in leaves if lf.kind == "fx"], key=lambda x: x[0]) + end = fade([e for k, e in fx], t_shade, min(0.08, 0.9 / max(len(fx), 1)), 0.25) + 0.25 + root.set("data-duration", f"{end:.2f}") + return end, {"units": len(units), "inks": round(t_inks, 2), "flats": round(t_flats, 2), + "shading": round(t_shade, 2), "end": round(end, 2), "speed": round(speed)} + + +def _inside(el, g): + for d in g.iter(): + if d is el: + return True + return False + + +def animate(svg_text, budget=12.0): + root = ET.fromstring(svg_text) + end, marks = animate_tree(root, budget) + return ET.tostring(root, encoding="unicode"), end + + +def build(src, out, budget=12.0): + tree = ET.parse(src) + end, marks = animate_tree(tree.getroot(), budget) + tree.write(out, encoding="unicode", xml_declaration=False) + return end, marks + + +def preview(svg, out, end, hold=2.0, fps=30, width=None): + """frames by seeking the SMIL clock, then ffmpeg -> mp4 (and a gif next to it)""" + import shutil + import subprocess + import tempfile + from playwright.sync_api import sync_playwright + root = ET.parse(svg).getroot() + w, h = int(float(root.get("width"))), int(float(root.get("height"))) + fd = tempfile.mkdtemp(prefix=".frames-", dir=os.path.dirname(os.path.abspath(out))) + n = int((end + hold) * fps) + with sync_playwright() as p: + b = p.chromium.launch() + pg = b.new_page(viewport={"width": w, "height": h}) + pg.goto("file://" + os.path.abspath(svg)) + pg.evaluate("document.documentElement.pauseAnimations()") + for k in range(n): + pg.evaluate(f"document.documentElement.setCurrentTime({min(k / fps, end + 0.01):.4f})") + pg.screenshot(path=f"{fd}/f{k:05d}.png") + b.close() + vf = f"scale={width}:-2:flags=lanczos," if width else "" + subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(fps), "-i", f"{fd}/f%05d.png", + "-vf", vf + "format=yuv420p", "-c:v", "libx264", "-crf", "18", out], check=True) + gif = out.rsplit(".", 1)[0] + ".gif" + subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", out, "-vf", + "fps=15,scale=480:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=96[p];[b][p]paletteuse", + gif], check=True) + shutil.rmtree(fd) + return out, gif + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("src") + ap.add_argument("out") + ap.add_argument("--budget", type=float, default=12.0, help="most seconds the line work may take") + ap.add_argument("--preview", default="") + a = ap.parse_args() + end, marks = build(a.src, a.out, a.budget) + print("wrote", a.out, marks) + if a.preview: + print("wrote", *preview(a.out, a.preview, end)) diff --git a/ventures/househand/favicon.svg b/ventures/househand/favicon.svg new file mode 100644 index 0000000..4e58e74 --- /dev/null +++ b/ventures/househand/favicon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ventures/househand/fonts/OFL.txt b/ventures/househand/fonts/OFL.txt new file mode 100644 index 0000000..d25934e --- /dev/null +++ b/ventures/househand/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Fredoka Project Authors (https://github.com/hafontia/Fredoka-One) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/ventures/househand/fonts/fredoka.woff2 b/ventures/househand/fonts/fredoka.woff2 new file mode 100644 index 0000000..746c682 Binary files /dev/null and b/ventures/househand/fonts/fredoka.woff2 differ diff --git a/ventures/househand/house.py b/ventures/househand/house.py new file mode 100644 index 0000000..3ddd875 --- /dev/null +++ b/ventures/househand/house.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python3 +"""house.py — the Teahub drawing hand as code. Everything the dojo has cleared so far, in one place. +Head (yellow), bean / tubes / hands / feet (green), garments + shadows + FX (blue), posed figures (brown). +House deviations from the field guide, set by the creative director: hands are 3 fingers + thumb (thumb 1.15×), feet have 4 toes.""" +import math, colorsys, os + +INK, LW = "#2B2135", 10 +FONT = "font-family=\"system-ui,-apple-system,sans-serif\"" +# field-guide pastels +PINK, YEL, ORANGE, MINT, SKY, CREAM, SKIN, SKIN_S, LAV, LAV_S = "#FFB3E6", "#FFE780", "#FFC897", "#B3FFC7", "#A1E9FF", "#FFEAD2", "#FFD0B8", "#E8A68B", "#C5C5FF", "#A4A4F4" +WHITE = "#FFFFFF" +# drawon.py replays a drawing stroke by stroke; with HOUSE_TAGS=1, figure() wraps each part in (and the +# whole figure in ) so the replay can draw a character the way an artist would: head first, feet last. +# Off by default: the output is byte-for-byte what it was. +TAGS = bool(os.environ.get("HOUSE_TAGS")) +def part(name, svg): return f'{svg}' if TAGS and svg else svg + +# ---------- colour ---------- +def hex2rgb(h): h = h.lstrip("#"); return tuple(int(h[i:i+2], 16)/255 for i in (0, 2, 4)) +def rgb2hex(r, g, b): return "#%02X%02X%02X" % tuple(max(0, min(255, round(c*255))) for c in (r, g, b)) +def shade(hexcol, sat_up=0.28, val_down=0.10): + """the house shadow: same hue, more saturated, a little darker (never grey).""" + h, s, v = colorsys.rgb_to_hsv(*hex2rgb(hexcol)) + return rgb2hex(*colorsys.hsv_to_rgb(h, min(1, s + sat_up), max(0, v - val_down))) +def hsv(hexcol): return colorsys.rgb_to_hsv(*hex2rgb(hexcol)) + +# ---------- svg atoms ---------- +def txt(x, y, s, size=22, w=650, op=1): + return f'{s}' +def outline(d, fill, lw=LW): + return f'' +def line(d, lw=LW, col=INK): + return f'' +def flat(d, fill, op=1, cls="fx"): + return f'' +def tube_ink(d, w): return line(d, w + 2*LW) +def tube_fill(d, w, col): return line(d, w, col) +def tube(d, w, col): return tube_ink(d, w) + tube_fill(d, w, col) +def circ(cx, cy, r, fill, op=1, cls="fx"): + return f'' +def ellipse_path(cx, cy, rx, ry, rot=0): + k = 0.5523; c, s = math.cos(math.radians(rot)), math.sin(math.radians(rot)) + def P(x, y): return f"{cx + x*c - y*s:.1f} {cy + x*s + y*c:.1f}" + return (f"M{P(-rx,0)} C{P(-rx,-ry*k)} {P(-rx*k,-ry)} {P(0,-ry)} C{P(rx*k,-ry)} {P(rx,-ry*k)} {P(rx,0)} " + f"C{P(rx,ry*k)} {P(rx*k,ry)} {P(0,ry)} C{P(-rx*k,ry)} {P(-rx,ry*k)} {P(-rx,0)} Z") +def blob(cx, cy, rx, ry, fill, lw=LW): return outline(ellipse_path(cx, cy, rx, ry), fill, lw) + +# ---------- bezier helpers ---------- +def bez(p0, p1, p2, p3, t): + u = 1-t + return (u*u*u*p0[0] + 3*u*u*t*p1[0] + 3*u*t*t*p2[0] + t*t*t*p3[0], + u*u*u*p0[1] + 3*u*u*t*p1[1] + 3*u*t*t*p2[1] + t*t*t*p3[1]) +def bez_tan(p0, p1, p2, p3, t): + u = 1-t + x = 3*u*u*(p1[0]-p0[0]) + 6*u*t*(p2[0]-p1[0]) + 3*t*t*(p3[0]-p2[0]) + y = 3*u*u*(p1[1]-p0[1]) + 6*u*t*(p2[1]-p1[1]) + 3*t*t*(p3[1]-p2[1]) + L = math.hypot(x, y) or 1 + return (x/L, y/L) +def split(p0, p1, p2, p3, t): + """de Casteljau: returns (first, second) cubic control tuples""" + def lerp(a, b): return (a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t) + a, b, c = lerp(p0, p1), lerp(p1, p2), lerp(p2, p3) + d, e = lerp(a, b), lerp(b, c); m = lerp(d, e) + return (p0, a, d, m), (m, e, c, p3) +def cpath(c): return f"M{c[0][0]:.1f} {c[0][1]:.1f} C{c[1][0]:.1f} {c[1][1]:.1f} {c[2][0]:.1f} {c[2][1]:.1f} {c[3][0]:.1f} {c[3][1]:.1f}" +def limb_curve(p, q, bow=0.0, lean=0.0): + """one cubic from p to q bending as a single curve. bow = sideways bulge (fraction of length, signed); + lean = how much the first handle leans along the chord (0.35 default).""" + dx, dy = q[0]-p[0], q[1]-p[1]; L = math.hypot(dx, dy) or 1 + nx, ny = -dy/L, dx/L + c1 = (p[0] + dx*0.33 + nx*bow*L, p[1] + dy*0.33 + ny*bow*L) + c2 = (p[0] + dx*0.67 + nx*bow*L, p[1] + dy*0.67 + ny*bow*L) + return (p, c1, c2, q) + +# ---------- head ---------- +_BASE = [("M",0,104),("C",-2,44,18,6,70,2),("C",90,1,110,1,130,2),("C",182,6,202,44,200,104), + ("C",202,124,202,144,200,164),("C",198,218,178,238,130,240),("C",110,241,90,241,70,240), + ("C",22,238,2,218,0,164),("C",-2,144,-2,124,0,104)] +_SHADOW = [("M",150,6),("C",190,24,204,100,200,164),("C",198,218,178,238,130,240),("C",170,226,184,190,180,140),("C",176,90,170,40,150,6)] +def _scale(seg, x, y, w, h, flip=1): + sx, sy = w/200, h/240; out = [] + for s in seg: + c = s[0]; n = s[1:] + pts = [] + for i in range(0, len(n), 2): + u = n[i] if flip == 1 else 200-n[i] + pts.append(f"{x+u*sx:.1f} {y+n[i+1]*sy:.1f}") + out.append(c + " ".join(pts)) + return " ".join(out) + " Z" +def head_path(x, y, w, h): return _scale(_BASE, x, y, w, h) +def head_shadow_path(x, y, w, h, side=1): return _scale(_SHADOW, x, y, w, h, side) + +def ear(x, cy, h, side=1, fill=SKIN): + r = h*0.06 + d = f"M{x} {cy-r} C{x+side*r*2.1} {cy-r*1.2} {x+side*r*2.1} {cy+r*1.2} {x} {cy+r} C{x-side*r*0.15} {cy+r*0.5} {x-side*r*0.15} {cy-r*0.5} {x} {cy-r} Z" + return outline(d, fill) + +def eye(cx, cy, s, kind="dot"): + if kind == "dot": return f'' + if kind == "far": return f'' + if kind == "wide": return f'' + if kind == "closed": return line(f"M{cx-s*0.55:.1f} {cy:.1f} C{cx-s*0.3:.1f} {cy+s*0.45:.1f} {cx+s*0.3:.1f} {cy+s*0.45:.1f} {cx+s*0.55:.1f} {cy:.1f}") + if kind == "happy": return line(f"M{cx-s*0.55:.1f} {cy+s*0.2:.1f} C{cx-s*0.3:.1f} {cy-s*0.35:.1f} {cx+s*0.3:.1f} {cy-s*0.35:.1f} {cx+s*0.55:.1f} {cy+s*0.2:.1f}") + if kind == "flat": return line(f"M{cx-s*0.55:.1f} {cy:.1f} C{cx-s*0.2:.1f} {cy+s*0.06:.1f} {cx+s*0.2:.1f} {cy+s*0.06:.1f} {cx+s*0.55:.1f} {cy:.1f}") + raise ValueError(kind) + +def mouth(cx, cy, w, kind="smile"): + h = w*0.35 + if kind == "none": return "" # reduced marks: eyes only + if kind == "smile": return line(f"M{cx-w/2:.1f} {cy:.1f} C{cx-w*0.25:.1f} {cy+h:.1f} {cx+w*0.25:.1f} {cy+h:.1f} {cx+w/2:.1f} {cy:.1f}") + if kind == "flat": return line(f"M{cx-w/2:.1f} {cy:.1f} C{cx-w*0.2:.1f} {cy+2:.1f} {cx+w*0.2:.1f} {cy+2:.1f} {cx+w/2:.1f} {cy:.1f}") + if kind == "small": return line(f"M{cx-w*0.25:.1f} {cy:.1f} C{cx-w*0.1:.1f} {cy+h*0.4:.1f} {cx+w*0.1:.1f} {cy+h*0.4:.1f} {cx+w*0.25:.1f} {cy:.1f}") + if kind == "o": return f'' + if kind == "open": return f'' + if kind == "grin": return line(f"M{cx-w*0.7:.1f} {cy-h*0.1:.1f} C{cx-w*0.3:.1f} {cy+h*0.9:.1f} {cx+w*0.3:.1f} {cy+h*0.9:.1f} {cx+w*0.7:.1f} {cy-h*0.1:.1f}") + if kind == "smirk": return line(f"M{cx-w*0.4:.1f} {cy+h*0.2:.1f} C{cx-w*0.1:.1f} {cy+h*0.5:.1f} {cx+w*0.3:.1f} {cy+h*0.3:.1f} {cx+w*0.5:.1f} {cy-h*0.3:.1f}") + if kind == "frown": return line(f"M{cx-w/2:.1f} {cy+h*0.6:.1f} C{cx-w*0.25:.1f} {cy-h*0.4:.1f} {cx+w*0.25:.1f} {cy-h*0.4:.1f} {cx+w/2:.1f} {cy+h*0.6:.1f}") + raise ValueError(kind) + +def head(x, y, w, h, fill=SKIN, view="front", eyes="dot", mouth_kind="smile", shadow=True, hair=None, uid="h", human_ears=True): + """view: front | q (3/4 facing viewer-left) | qr (3/4 facing right) | side | sider | back. + hair: (colour, kind) kind in tuft|cap|bob|bun|none. human_ears=False for characters whose ears live elsewhere + (bunny ears are drawn behind the head by the caller). returns svg.""" + ear_ = ear if human_ears else (lambda *a: "") + out = [] + hp = head_path(x, y, w, h) + out.append(outline(hp, fill)) + if shadow: + side = 1 + out.append(f'{flat(head_shadow_path(x, y, w, h, side), shade(fill), cls="s")}') + ey = y + h/3; s = h*0.085; my = ey + h*0.10 + if view == "front": + gap = w*0.36 + out += [ear_(x+w, y+h/2, h, 1, fill), ear_(x, y+h/2, h, -1, fill), eye(x+w/2-gap/2, ey, s, eyes), eye(x+w/2+gap/2, ey, s, eyes), mouth(x+w/2, my, gap*0.9, mouth_kind)] + elif view in ("q", "qr"): + f = 1 if view == "q" else -1 + nx = x + w*(0.6 if f == 1 else 0.4); fx = x + w*(0.2 if f == 1 else 0.8) + out += [ear_(x+w, y+h/2, h, 1, fill) if f == 1 else ear_(x, y+h/2, h, -1, fill), eye(nx, ey, s, eyes), eye(fx, ey, s, "far" if eyes == "dot" else eyes), mouth(x+w/2 - f*w*0.12, my, w*0.24, mouth_kind)] + elif view in ("side", "sider"): + f = 1 if view == "side" else -1 + ex = x + w*(0.2 if f == 1 else 0.8); e0 = x+3 if f == 1 else x+w-3; hm = w*0.16 + out += [ear_(x+w*0.5, y+h/2, h, f, fill), eye(ex, ey, s, eyes), line(f"M{e0:.1f} {my:.1f} C{e0+f*hm*0.4:.1f} {my+hm*0.5:.1f} {e0+f*hm*0.8:.1f} {my+hm*0.5:.1f} {e0+f*hm:.1f} {my+hm*0.1:.1f}")] + elif view == "back": + out += [ear_(x+w, y+h/2, h, 1, fill), ear_(x, y+h/2, h, -1, fill)] + out = [o for o in out if o] + if hair: out.append(hair_mass(x, y, w, h, hair[0], hair[1], view)) + return "\n".join(out) + +def hair_mass(x, y, w, h, col, kind, view="front"): + """hair as one closed mass sitting on the top of the oblong; the mass is the identity cue.""" + if kind == "none": return "" + if kind == "tuft": + tx = x + w*0.6 + return line(f"M{tx:.1f} {y+2:.1f} C{tx:.1f} {y-h*0.06:.1f} {tx+w*0.08:.1f} {y-h*0.08:.1f} {tx+w*0.1:.1f} {y-h*0.14:.1f}", LW+4, INK) + line(f"M{tx:.1f} {y+2:.1f} C{tx:.1f} {y-h*0.06:.1f} {tx+w*0.08:.1f} {y-h*0.08:.1f} {tx+w*0.1:.1f} {y-h*0.14:.1f}", LW-4, col) + if kind == "cap": # a beanie: dome + band + d = f"M{x-6:.1f} {y+h*0.16:.1f} C{x-8:.1f} {y-h*0.5:.1f} {x+w+8:.1f} {y-h*0.5:.1f} {x+w+6:.1f} {y+h*0.16:.1f} C{x+w*0.7:.1f} {y+h*0.12:.1f} {x+w*0.3:.1f} {y+h*0.12:.1f} {x-6:.1f} {y+h*0.16:.1f} Z" + band = f"M{x-8:.1f} {y+h*0.1:.1f} C{x+w*0.3:.1f} {y+h*0.18:.1f} {x+w*0.7:.1f} {y+h*0.18:.1f} {x+w+8:.1f} {y+h*0.1:.1f} C{x+w+8:.1f} {y+h*0.18:.1f} {x+w+8:.1f} {y+h*0.22:.1f} {x+w+6:.1f} {y+h*0.24:.1f} C{x+w*0.7:.1f} {y+h*0.3:.1f} {x+w*0.3:.1f} {y+h*0.3:.1f} {x-6:.1f} {y+h*0.24:.1f} C{x-8:.1f} {y+h*0.22:.1f} {x-8:.1f} {y+h*0.18:.1f} {x-8:.1f} {y+h*0.1:.1f} Z" + return outline(d, col) + outline(band, shade(col, 0.2, 0.06)) + if kind == "bob": # helmet of hair to the ears; bangs stay above the eye line + d = (f"M{x-10:.1f} {y+h*0.5:.1f} C{x-14:.1f} {y+h*0.1:.1f} {x+w*0.2:.1f} {y-h*0.1:.1f} {x+w*0.5:.1f} {y-h*0.1:.1f} " + f"C{x+w*0.8:.1f} {y-h*0.1:.1f} {x+w+14:.1f} {y+h*0.1:.1f} {x+w+10:.1f} {y+h*0.5:.1f} " + f"C{x+w+2:.1f} {y+h*0.3:.1f} {x+w*0.96:.1f} {y+h*0.08:.1f} {x+w*0.75:.1f} {y+h*0.1:.1f} " + f"C{x+w*0.4:.1f} {y+h*0.12:.1f} {x+w*0.1:.1f} {y+h*0.12:.1f} {x-10:.1f} {y+h*0.5:.1f} Z") + return outline(d, col) + if kind == "bun": # swept mass + a ball on top + d = f"M{x-4:.1f} {y+h*0.3:.1f} C{x-6:.1f} {y-h*0.04:.1f} {x+w*0.3:.1f} {y-h*0.1:.1f} {x+w*0.55:.1f} {y-h*0.08:.1f} C{x+w*0.85:.1f} {y-h*0.06:.1f} {x+w+6:.1f} {y+h*0.05:.1f} {x+w+4:.1f} {y+h*0.3:.1f} C{x+w*0.7:.1f} {y+h*0.18:.1f} {x+w*0.3:.1f} {y+h*0.18:.1f} {x-4:.1f} {y+h*0.3:.1f} Z" + return outline(d, col) + blob(x+w*0.62, y-h*0.12, w*0.17, w*0.15, col) + if kind == "spikes": # three rounded spikes + d = f"M{x+w*0.1:.1f} {y+h*0.12:.1f} C{x+w*0.12:.1f} {y-h*0.1:.1f} {x+w*0.26:.1f} {y-h*0.12:.1f} {x+w*0.3:.1f} {y+h*0.04:.1f} C{x+w*0.36:.1f} {y-h*0.18:.1f} {x+w*0.58:.1f} {y-h*0.2:.1f} {x+w*0.6:.1f} {y+h*0.02:.1f} C{x+w*0.68:.1f} {y-h*0.14:.1f} {x+w*0.9:.1f} {y-h*0.1:.1f} {x+w*0.9:.1f} {y+h*0.12:.1f} C{x+w*0.6:.1f} {y+h*0.06:.1f} {x+w*0.4:.1f} {y+h*0.06:.1f} {x+w*0.1:.1f} {y+h*0.12:.1f} Z" + return outline(d, col) + return "" + +# ---------- body ---------- +def bean_path(cx, y0, y1, r, sw, lean=0.0): + """torso: belly circle radius r sitting on y1, shoulders width sw at y0; lean shifts the shoulders sideways + (fraction of torso height) so the back is one arc — the line of action.""" + H = y1 - y0; dx = lean*H; sh = sw*0.14; k = 0.55 + return (f"M{cx-sw/2+dx:.1f} {y0+sh:.1f} C{cx-sw/2+dx:.1f} {y0:.1f} {cx+sw/2+dx:.1f} {y0:.1f} {cx+sw/2+dx:.1f} {y0+sh:.1f} " + f"C{cx+sw/2+dx*0.6:.1f} {y0+H*0.42:.1f} {cx+r:.1f} {y1-r*1.55:.1f} {cx+r:.1f} {y1-r:.1f} " + f"C{cx+r:.1f} {y1-r*(1-k):.1f} {cx+r*k:.1f} {y1:.1f} {cx:.1f} {y1:.1f} " + f"C{cx-r*k:.1f} {y1:.1f} {cx-r:.1f} {y1-r*(1-k):.1f} {cx-r:.1f} {y1-r:.1f} " + f"C{cx-r:.1f} {y1-r*1.55:.1f} {cx-sw/2+dx*0.6:.1f} {y0+H*0.42:.1f} {cx-sw/2+dx:.1f} {y0+sh:.1f} Z") + +def shoe(x, y, w, h, flip=1, fill=YEL): + """two masses: upper (one closed shape) + sole band ~18% of height, thicker at the heel, bowed. toe points toward +flip.""" + sole = h*0.18; heel = sole*1.35 + tx = x + flip*w; hx = x + up = (f"M{hx:.1f} {y-heel:.1f} C{hx:.1f} {y-h:.1f} {hx+flip*w*0.25:.1f} {y-h:.1f} {hx+flip*w*0.45:.1f} {y-h*0.92:.1f} " + f"C{hx+flip*w*0.75:.1f} {y-h*0.8:.1f} {tx:.1f} {y-h*0.55:.1f} {tx:.1f} {y-sole*0.9:.1f} " + f"C{tx-flip*w*0.3:.1f} {y-sole*0.7:.1f} {hx+flip*w*0.3:.1f} {y-sole*0.8:.1f} {hx:.1f} {y-heel:.1f} Z") + sl = (f"M{hx:.1f} {y-heel:.1f} C{hx+flip*w*0.3:.1f} {y-heel*0.9:.1f} {tx-flip*w*0.3:.1f} {y-sole*0.8:.1f} {tx:.1f} {y-sole*0.9:.1f} " + f"C{tx+flip*2:.1f} {y-sole*0.3:.1f} {tx-flip*w*0.1:.1f} {y:.1f} {tx-flip*w*0.2:.1f} {y:.1f} " + f"C{hx+flip*w*0.4:.1f} {y+2:.1f} {hx+flip*w*0.1:.1f} {y-2:.1f} {hx+flip*4:.1f} {y-heel*0.35:.1f} " + f"C{hx-flip*2:.1f} {y-heel*0.6:.1f} {hx-flip*2:.1f} {y-heel*0.8:.1f} {hx:.1f} {y-heel:.1f} Z") + return outline(up, fill) + outline(sl, WHITE) + line(f"M{hx+flip*w*0.42:.1f} {y-h*0.9:.1f} C{hx+flip*w*0.5:.1f} {y-h*0.7:.1f} {hx+flip*w*0.5:.1f} {y-h*0.55:.1f} {hx+flip*w*0.46:.1f} {y-h*0.42:.1f}") + +def mitt(cx, cy, r, fill=SKIN): return blob(cx, cy, r, r*1.15, fill) + +def finger_curve(p, q, bend=0.0): + mx, my = (p[0]+q[0])/2, (p[1]+q[1])/2 + nx, ny = -(q[1]-p[1]), (q[0]-p[0]); L = math.hypot(nx, ny) or 1; nx, ny = nx/L, ny/L + c = (mx + nx*bend*L, my + ny*bend*L) + return f"M{p[0]:.1f} {p[1]:.1f} Q{c[0]:.1f} {c[1]:.1f} {q[0]:.1f} {q[1]:.1f}" + +def hand(cx, cy, pose="open", fw=24, prop=None, rot=0, fill=SKIN, wrist=True, scale=1.0, palm=1.0): + """house hand: 3 fingers + thumb. palm = soft disc r≈1.35·fw. thumb 1.15× finger width, shorter, low on the palm. + rot rotates the whole hand (degrees, 0 = fingers up). grips draw far fingers → prop → palm + near fingers. + palm < 1 shrinks only the disc (fingers keep their roots and reach): use it when scale > 1 makes the palm wider + than the wrist, or the disc shows as a fifth lobe in the silhouette.""" + fw = fw*scale; R = fw*1.35; TW = fw*1.15; RP = R*palm + def F(ang, length, bend=0.0, w=fw): + a = math.radians(ang + rot) + p = (cx + math.cos(a)*R*0.55, cy - math.sin(a)*R*0.55) + q = (cx + math.cos(a)*(R*0.55+length), cy - math.sin(a)*(R*0.55+length)) + return (finger_curve(p, q, bend), w) + fingers, far, near, thumb = [], [], [], None + if pose == "open": fingers = [F(a, fw*2.4) for a in (112, 84, 56)]; thumb = F(12, fw*1.7, 0, TW) + elif pose == "point": fingers = [F(96, fw*2.8)] + [F(a, fw*0.85) for a in (62, 36)]; thumb = F(140, fw*1.4, 0, TW) + elif pose == "fist": fingers = [F(a, fw*0.8) for a in (112, 78, 44)]; thumb = F(150, fw*1.1, 0, TW) + elif pose == "peace": fingers = [F(106, fw*2.7), F(74, fw*2.7), F(38, fw*0.85)]; thumb = F(160, fw*1.3, 0, TW) + elif pose == "pinch": fingers = [F(84, fw*2.3, -0.22), F(50, fw*0.85), F(26, fw*0.8)]; thumb = F(122, fw*2.1, 0.24, TW) + elif pose == "thumbsup":fingers = [F(a, fw*0.85) for a in (22, -2, -26)]; thumb = F(95, fw*2.2, 0, TW) + elif pose == "grip": far = [F(118, fw*2.0, 0.22)]; near = [F(a, fw*2.0, 0.22) for a in (88, 58)]; thumb = F(150, fw*1.6, -0.15, TW) + elif pose == "wave": fingers = [F(a, fw*2.4, 0.08) for a in (120, 92, 64)]; thumb = F(20, fw*1.6, 0, TW) + palm = outline(ellipse_path(cx, cy, RP, RP*1.15), fill) + wr = tube(f"M{cx:.1f} {cy+R*0.6:.1f} C{cx:.1f} {cy+R*1.3:.1f} {cx-4:.1f} {cy+R*1.8:.1f} {cx-6:.1f} {cy+R*2.3:.1f}", R*1.5, fill) if wrist else "" + def ink(fs): return "".join(tube_ink(d, w) for d, w in fs) + def fil(fs): return "".join(tube_fill(d, w, fill) for d, w in fs) + allf = fingers + ([thumb] if thumb else []) + if pose != "grip": + return "\n".join([wr, ink(allf), fil(allf), palm, fil(allf)]) + return "\n".join([wr, ink(far), fil(far), prop or "", ink(near + [thumb]), fil(near + [thumb]), palm, fil(near + [thumb])]) + +def bare_foot(cx, cy, view="side", flip=1, fill=SKIN): + """house foot: 4 toes, big toe ~1.6×. ankle = continuous tube.""" + out = [] + def lobe(x, y, r): return blob(x, y, r, r*1.2, fill) + if view == "side": + L, H = 200, 70; hx, tx = cx-L*0.4, cx+L*0.5 + out.append(tube(f"M{hx+42:.1f} {cy-H*0.9:.1f} C{hx+40:.1f} {cy-H*1.6:.1f} {hx+36:.1f} {cy-H*2.2:.1f} {hx+34:.1f} {cy-H*2.9:.1f}", 54, fill)) + out.append(outline(f"M{hx:.1f} {cy-H:.1f} C{hx-16:.1f} {cy-H*0.6:.1f} {hx-10:.1f} {cy-6:.1f} {hx+30:.1f} {cy:.1f} C{cx:.1f} {cy+2:.1f} {cx+40:.1f} {cy:.1f} {tx-40:.1f} {cy:.1f} C{tx-10:.1f} {cy+2:.1f} {tx+4:.1f} {cy-10:.1f} {tx:.1f} {cy-22:.1f} C{tx-10:.1f} {cy-44:.1f} {cx+30:.1f} {cy-H*0.75:.1f} {cx-10:.1f} {cy-H*0.95:.1f} Z", fill)) + for dx, dy, r in ((-86,-30,13),(-62,-27,15),(-38,-24,17)): out.append(lobe(tx+dx, cy+dy, r)) + out.append(lobe(tx, cy-20, 26)) + elif view == "front": + W = 140 + out.append(tube(f"M{cx:.1f} {cy-50:.1f} C{cx:.1f} {cy-110:.1f} {cx-2:.1f} {cy-160:.1f} {cx-4:.1f} {cy-220:.1f}", 56, fill)) + out.append(outline(f"M{cx-W/2:.1f} {cy-40:.1f} C{cx-W/2-6:.1f} {cy-80:.1f} {cx+W/2+6:.1f} {cy-80:.1f} {cx+W/2:.1f} {cy-40:.1f} C{cx+W/2+4:.1f} {cy-10:.1f} {cx+W/2:.1f} {cy+4:.1f} {cx+W/2-20:.1f} {cy+6:.1f} L{cx-W/2+20:.1f} {cy+6:.1f} C{cx-W/2:.1f} {cy+4:.1f} {cx-W/2-4:.1f} {cy-10:.1f} {cx-W/2:.1f} {cy-40:.1f} Z", fill)) + for dx, r in ((46, 13), (22, 14), (-6, 15), (-42, 24)): out.append(lobe(cx+dx, cy+4, r)) + return "\n".join(out) + +# ---------- garments ---------- +def opening(cx, cy, rx, ry, fill, rot=0, lw=LW): + """an ellipse with a lip: the opening of a tube (cuff, hem, collar). the lip is a second contour inside.""" + return outline(ellipse_path(cx, cy, rx, ry, rot), shade(fill), lw) + line(_arc_inner(cx, cy, rx*0.72, ry*0.55, rot), lw) +def _arc_inner(cx, cy, rx, ry, rot): + k = 0.5523; c, s = math.cos(math.radians(rot)), math.sin(math.radians(rot)) + def P(x, y): return f"{cx + x*c - y*s:.1f} {cy + x*s + y*c:.1f}" + return f"M{P(-rx,0)} C{P(-rx*k,ry)} {P(rx*k,ry)} {P(rx,0)}" + +def sleeve(curve, limb_w, col, frac=0.55, ratio=1.7, cuff_ry=0.3, tuck=False): + """garment over the first `frac` of a limb curve: a tube ratio× the limb width ending in a cuff (ellipse + lip). + cuff_ry: the opening's minor axis as a fraction of the sleeve width; on small figures a rounder cuff (~0.42) keeps + the lip readable inside the constant line weight. tuck: end the tube just short of the opening's centre, so its + round end of ink stays inside the opening instead of showing as a crescent below it (small figures).""" + a, b = split(*curve, frac) + w = limb_w*ratio + end = a[3]; tx, ty = bez_tan(*curve, frac) + rot = math.degrees(math.atan2(ty, tx)) + 90 + if tuck: + d = max(0.0, w/2 + LW*0.5 - w*cuff_ry) # ink cap radius (w/2 + LW) minus the opening's reach (ry + LW/2) + lo, hi = 0.0, frac + for _ in range(30): + m = (lo + hi)/2 + if math.dist(bez(*curve, m), end) > d: lo = m + else: hi = m + a = split(*curve, lo)[0] + return tube(cpath(a), w, col) + band(a, w*0.08, w*0.46, shade(col)) + opening(end[0], end[1], w/2 + LW*0.4, w*cuff_ry, col, rot) + +def band(c, inner, outer, col, n=14, t0=0.06, t1=0.94): + """a strokeless shadow band along the right-hand side of a cubic tube: polygon between two normal offsets.""" + pts_o, pts_i = [], [] + for k in range(n+1): + t = t0 + (t1-t0)*k/n + x, y = bez(*c, t); tx, ty = bez_tan(*c, t) + nx, ny = -ty, tx + if nx < 0: nx, ny = -nx, -ny # the side facing +x (screen right) + pts_o.append((x + nx*outer, y + ny*outer)); pts_i.append((x + nx*inner, y + ny*inner)) + d = "M" + " L".join(f"{x:.1f} {y:.1f}" for x, y in pts_o) + " L" + " L".join(f"{x:.1f} {y:.1f}" for x, y in reversed(pts_i)) + " Z" + return flat(d, col, 1, "s") + +def fold(p, q, bow=0.12): + """one curved round-capped dash that touches neither contour""" + return line(finger_curve(p, q, bow)) + +# ---------- FX (strokeless) ---------- +def lobes(cx, cy, pts, fill, op=1): + """a cloud/plume built from overlapping discs; pts = [(dx, dy, r)]""" + return "".join(circ(cx+dx, cy+dy, r, fill, op) for dx, dy, r in pts) + +def sparkle(cx, cy, r, fill=WHITE, op=1): + """four-pointed, concave sides""" + k = r*0.18 + return flat(f"M{cx:.1f} {cy-r:.1f} Q{cx+k:.1f} {cy-k:.1f} {cx+r:.1f} {cy:.1f} Q{cx+k:.1f} {cy+k:.1f} {cx:.1f} {cy+r:.1f} Q{cx-k:.1f} {cy+k:.1f} {cx-r:.1f} {cy:.1f} Q{cx-k:.1f} {cy-k:.1f} {cx:.1f} {cy-r:.1f} Z", fill, op) + +def drop(cx, cy, r, fill, op=1, tilt=0): + """a droplet: round bottom, soft point up""" + c, s = math.cos(math.radians(tilt)), math.sin(math.radians(tilt)) + def P(x, y): return f"{cx + x*c - y*s:.1f} {cy + x*s + y*c:.1f}" + return flat(f"M{P(0,-r*1.6)} C{P(r*0.5,-r*0.6)} {P(r,-r*0.2)} {P(r,r*0.3)} C{P(r,r*0.9)} {P(r*0.55,r*1.3)} {P(0,r*1.3)} C{P(-r*0.55,r*1.3)} {P(-r,r*0.9)} {P(-r,r*0.3)} C{P(-r,-r*0.2)} {P(-r*0.5,-r*0.6)} {P(0,-r*1.6)} Z", fill, op) + +def flame(cx, cy, w, h, cols): + """fire: stacked values, each inner shape echoing the outer silhouette at ~50-60% scale, bottom-aligned""" + out = [] + for i, col in enumerate(cols): + sc = 1 - i*0.42 + ww, hh = w*sc, h*sc + out.append(flat(f"M{cx:.1f} {cy-hh:.1f} C{cx+ww*0.2:.1f} {cy-hh*0.75:.1f} {cx+ww*0.55:.1f} {cy-hh*0.55:.1f} {cx+ww*0.5:.1f} {cy-hh*0.25:.1f} C{cx+ww*0.48:.1f} {cy-hh*0.05:.1f} {cx+ww*0.3:.1f} {cy:.1f} {cx:.1f} {cy:.1f} C{cx-ww*0.3:.1f} {cy:.1f} {cx-ww*0.48:.1f} {cy-hh*0.05:.1f} {cx-ww*0.5:.1f} {cy-hh*0.25:.1f} C{cx-ww*0.55:.1f} {cy-hh*0.55:.1f} {cx-ww*0.2:.1f} {cy-hh*0.75:.1f} {cx:.1f} {cy-hh:.1f} Z", col)) + return "".join(out) + +def splash(cx, cy, r, fill, hi=None): + """main mass + detached droplets at descending sizes""" + out = [flat(f"M{cx-r:.1f} {cy:.1f} C{cx-r*0.9:.1f} {cy-r*0.9:.1f} {cx-r*0.3:.1f} {cy-r*1.1:.1f} {cx-r*0.1:.1f} {cy-r*0.6:.1f} C{cx+r*0.1:.1f} {cy-r*1.0:.1f} {cx+r*0.7:.1f} {cy-r*1.0:.1f} {cx+r*0.6:.1f} {cy-r*0.4:.1f} C{cx+r*1.1:.1f} {cy-r*0.4:.1f} {cx+r*1.1:.1f} {cy+r*0.3:.1f} {cx+r*0.7:.1f} {cy+r*0.3:.1f} C{cx+r*0.3:.1f} {cy+r*0.5:.1f} {cx-r*0.5:.1f} {cy+r*0.5:.1f} {cx-r:.1f} {cy:.1f} Z", fill)] + for dx, dy, rr, t in ((-1.25, -0.9, 0.22, -30), (-0.2, -1.5, 0.17, 0), (0.9, -1.3, 0.13, 20), (1.4, -0.3, 0.1, 60)): + out.append(drop(cx+dx*r, cy+dy*r, rr*r, fill, 1, t)) + if hi: out.append(flat(ellipse_path(cx-r*0.3, cy-r*0.15, r*0.3, r*0.14, -20), hi)) + return "".join(out) + +# ---------- the figure ---------- +def figure(cx, ground, head_h=110, heads=4.7, belly_r=62, sw_f=0.72, lw=30, view="front", + skin=SKIN, shirt=LAV, pants=SKY, shoes=YEL, hair=None, garments="volume", + lean=0.0, head_dx=0.0, head_dy=0.0, arms=None, legs=None, hands=("open", "open"), hand_rot=(0, 0), + eyes="dot", mouth_kind="smile", uid="f", shadow=True, ground_shadow=False, hip_dx=0.0, labels=False, + hand_scale=(1.0, 1.0), props=(None, None), accessories=(), ground_shadow_w=2.2, + human_ears=True, skirt=None, legwear=None, under=None, over=None, mid=None, hand_palm=(1.0, 1.0), + sleeve_frac=0.55, sleeve_ratio=1.7, skirt_len=0.36, skirt_flare=1.32, cuff_ry=0.3, + shoe_fn=None, shoes_under=False, torso_fold=True, leg_stack=False, hem_frac=0.86, hem_ry=None, + garment_tuck=False, collar=True, skirt_fold=True): + """a whole on-model character. heads = height in heads. lean = torso lean (fraction, +right). + arms/legs = ((dx,dy),(dx,dy)) hand/foot targets relative to the shoulder/hip, in units of lw; None = hanging. + garments: 'volume' (sleeves/trousers 1.7×, cuffs with lips) | 'skin' (same width as limb, no openings) | 'none'. + skirt: a colour → flared skirt from the waist with a hem opening (ellipse + lip); legs come out of the hem in + `legwear` (default skin) instead of trousers. under/over: callables(g) → svg drawn first / last, where g is the + figure's geometry (head box, shoulders, hips, view) — for parts that live behind or on top of everything + (bunny ears, tails, custom hair). mid: callable(g) drawn just under the torso — above the skirt and the + far arm of a ¾ view (a tail that belongs to the body, not behind the arm). props: svg strings, or callables(x, y, rot, fw) placed at the hand. + shoe_fn: callable(x, y, w, h, flip, fill) drawn instead of shoe(). shoes_under: draw the shoes before the trousers, so + each hem opening sits over its boot top. torso_fold: False drops the shirt's fold dash (a back view whose hair covers + it). leg_stack: in profile, put both legs on one line so the far leg hides exactly behind the near one instead of + peeking out as a sliver of solid ink. hem_frac / hem_ry: where the trouser hems sit along the leg and their opening's + minor axis (default: cuff_ry) — a higher, flatter hem leaves more of the boot below it. garment_tuck: end sleeves + and trouser legs inside their openings (see sleeve()). collar=False drops the neck opening: on a small figure the + head hides its upper half and what shows below the chin (outline + lip) closes up into a lens of solid ink. + skirt_fold=False drops the skirt's fold dash (when a tail or prop sits where it would start).""" + H = head_h*heads; head_w = head_h/1.2 + legs_len = H*0.48 + hip_y = ground - legs_len + head_y = ground - H + head_dy + y0 = head_y + head_h*0.88 - head_dy + vw = {"front": 1.0, "back": 1.0, "q": 0.93, "qr": 0.93, "qb": 0.93, "qbr": 0.93, "side": 0.78, "sider": 0.78}.get(view, 1.0) + r = belly_r*vw; y1 = hip_y + r*0.3 + sw = min(2*r*sw_f, 2*r) + head_w = head_w*vw + hview = {"qb": "back", "qbr": "back"}.get(view, view) + dx_sh = lean*(y1 - y0) # shoulder shift from lean + g = dict(cx=cx, ground=ground, view=view, head_x=cx + dx_sh + head_dx - head_w/2, head_y=head_y, head_w=head_w, + head_h=head_h, y0=y0, y1=y1, r=r, sw=sw, hip_y=hip_y, lw=lw, dx_sh=dx_sh, legs_len=legs_len, uid=uid) + out = [] + if ground_shadow: + out.append(part("ground", flat(ellipse_path(cx, ground+6, r*ground_shadow_w, lw*0.55), shade(CREAM, 0.25, 0.08), 0.9, "s"))) + if under: + out.append(part("under", under(g))) + # ---- legs ---- + leg_pts = [] + for i, s in enumerate((-1, 1)): + sx = 1 if (leg_stack and vw <= 0.8) else s # stacked profile: both legs take the near leg's line + hx = cx + hip_dx + sx*r*(0.45 if vw > 0.8 else 0.18); hy = y1 - r*0.5 + if legs and legs[i]: + fx, fy = hx + legs[i][0]*lw, hy + legs[i][1]*lw + else: + fx, fy = cx + hip_dx + sx*((r*0.55+lw*0.3) if vw > 0.8 else lw*0.35), ground - lw*0.9 + bow = 0.09*sx if not (legs and legs[i]) else 0.16*sx + leg_pts.append((limb_curve((hx, hy), (fx, fy), bow), s, fx, fy)) + if skirt: + hem_y = (y1 - r*0.5) + (ground - (y1 - r*0.5))*skirt_len + a, b = r*0.86, r*skirt_flare; ry = b*0.2; y_w = y1 - r*1.1; k = 0.5523 + body = (f"M{cx-a:.1f} {y_w:.1f} C{cx-a-r*0.1:.1f} {y_w+(hem_y-y_w)*0.45:.1f} {cx-b+r*0.04:.1f} {hem_y-(hem_y-y_w)*0.25:.1f} {cx-b:.1f} {hem_y:.1f} " + f"C{cx-b:.1f} {hem_y+ry*k:.1f} {cx-b*k:.1f} {hem_y+ry:.1f} {cx:.1f} {hem_y+ry:.1f} " + f"C{cx+b*k:.1f} {hem_y+ry:.1f} {cx+b:.1f} {hem_y+ry*k:.1f} {cx+b:.1f} {hem_y:.1f} " + f"C{cx+b-r*0.04:.1f} {hem_y-(hem_y-y_w)*0.25:.1f} {cx+a+r*0.1:.1f} {y_w+(hem_y-y_w)*0.45:.1f} {cx+a:.1f} {y_w:.1f} Z") + out.append(part("skirt", outline(body, skirt))) + if shadow: + out.append(part("skirt", f'' + + flat(f"M{cx+a*0.3:.1f} {y_w:.1f} C{cx+b*0.62:.1f} {y_w+(hem_y-y_w)*0.4:.1f} {cx+b*0.58:.1f} {hem_y-ry:.1f} {cx+b*0.5:.1f} {hem_y+ry*2:.1f} L{cx+b+30:.1f} {hem_y+ry*2:.1f} L{cx+b+30:.1f} {y_w:.1f} Z", shade(skirt), 1, "s") + "")) + if view != "back" and skirt_fold: # the back view keeps at most one fold line (the bodice's) + out.append(part("skirt", fold((cx-b*0.62, hem_y-(hem_y-y_w)*0.42), (cx-b*0.74, hem_y-ry*1.4), 0.12))) + g.update(hem_y=hem_y, hem_rx=b) + for cur, s, fx, fy in leg_pts: # the legs come out of the hem: start them inside it... + lo, hi = 0.0, 1.0 + for _ in range(30): + m = (lo + hi)/2 + if bez(*cur, m)[1] < hem_y - ry + lw*0.5 + LW*0.5 + 2: lo = m # the round top stays inside the opening + else: hi = m + out.append(part("legs", tube(cpath(split(*cur, lo)[1]), lw, legwear or skin))) + out.append(part("skirt", opening(cx, hem_y, b, ry, skirt))) # ...and lay the opening over their tops, as a cuff sits over a wrist + def draw_shoes(): + for cur, s, fx, fy in leg_pts: + if view in ("front", "back", "q", "qr"): + flip = s if view in ("front", "back") else (1 if view == "qr" else -1) + else: + flip = 1 if view == "sider" else -1 + out.append(part("shoes", (shoe_fn or shoe)(fx - flip*lw*0.7, fy + lw*0.9, lw*2.4, lw*1.5, flip, shoes))) + if not skirt: + for cur, s, fx, fy in leg_pts: + out.append(part("legs", tube(cpath(cur), lw, pants if garments == "skin" else skin))) + if shoes_under: draw_shoes() + if garments == "volume": + for cur, s, fx, fy in leg_pts: + out.append(part("legs", sleeve(cur, lw, pants, frac=hem_frac, ratio=1.6, cuff_ry=cuff_ry if hem_ry is None else hem_ry, tuck=garment_tuck))) + if skirt or not shoes_under: draw_shoes() + if mid: + out.append(part("mid", mid(g))) + # ---- torso ---- + bp = bean_path(cx, y0, y1, r, sw, lean) + out.append(part("torso", outline(bp, shirt if garments != "none" else skin))) + if shadow: + out.append(part("torso", f'' + + flat(f"M{cx+r*0.3:.1f} {y0:.1f} C{cx+r*1.2:.1f} {y0+(y1-y0)*0.3:.1f} {cx+r*1.1:.1f} {y1-r*0.4:.1f} {cx:.1f} {y1+4:.1f} L{cx+r+20:.1f} {y1+4:.1f} L{cx+r+20:.1f} {y0-20:.1f} Z", shade(shirt if garments != "none" else skin), 1, "s") + "")) + if garments == "volume": + # collar: ellipse with a lip at the shoulders, and one hem lip at the bean bottom + out.append(part("torso", line(f"M{cx-r*0.55:.1f} {y1-r*0.12:.1f} C{cx-r*0.2:.1f} {y1+r*0.02:.1f} {cx+r*0.2:.1f} {y1+r*0.02:.1f} {cx+r*0.55:.1f} {y1-r*0.12:.1f}"))) + if torso_fold: out.append(part("torso", fold((cx-r*0.5+dx_sh*0.5, y0+(y1-y0)*0.45), (cx-r*0.2, y1-r*0.6), 0.1))) + # ---- arms ---- + arm_len = (y1 + legs_len*0.22) - (y0 + sw*0.1) + arm_pts = [] + for i, s in enumerate((-1, 1)): + ax = cx + dx_sh + s*(sw/2 - lw*0.3); ay = y0 + sw*0.12 + if arms and arms[i]: + hx, hy = ax + arms[i][0]*lw, ay + arms[i][1]*lw + bow = -0.2*s + else: + hx, hy = cx + s*(r + lw*0.7), ay + arm_len + bow = 0.1*s + arm_pts.append((limb_curve((ax, ay), (hx, hy), bow), s, hx, hy)) + def draw_arm(i): + cur, s, hx, hy = arm_pts[i] + o = [tube(cpath(cur), lw, skin)] + if garments == "skin": o.append(tube_fill(cpath(split(*cur, 0.55)[0]), lw, shirt)) + if garments == "volume": o.append(sleeve(cur, lw, shirt, frac=sleeve_frac, ratio=sleeve_ratio, cuff_ry=cuff_ry, tuck=garment_tuck)) + tx, ty = bez_tan(*cur, 1.0) + rot = math.degrees(math.atan2(-ty, tx)) - 90 # fingers continue along the limb + px, py = hx + tx*lw*0.12, hy + ty*lw*0.12 + pr = props[i](px, py, rot + hand_rot[i], lw*0.38*hand_scale[i]) if callable(props[i]) else props[i] + o.append(hand(px, py, hands[i], fw=lw*0.38, rot=rot + hand_rot[i], fill=skin, wrist=False, scale=hand_scale[i], prop=pr, palm=hand_palm[i])) + return "\n".join(o) + near = {"q": 0, "qb": 1, "side": 0, "qr": 1, "qbr": 0, "sider": 1}.get(view) + far_first = view in ("q", "qr", "qb", "qbr") + if far_first: + # far arm goes behind the torso: re-insert before the torso block + torso_idx = len(out) - (3 if garments == "volume" else 1) - (1 if shadow else 0) - (1 if mid else 0) + out.insert(torso_idx, part("arm-far", draw_arm(1 - near))) + out.append(part("arm-near", draw_arm(near))) + elif view in ("side", "sider"): + out.append(part("arm-near", draw_arm(near))) + else: + out.append(part("arm", draw_arm(0))); out.append(part("arm", draw_arm(1))) + if garments == "volume" and collar: + out.append(part("collar", opening(cx + dx_sh, y0 + sw*0.16, head_w*0.66, sw*0.13, shirt))) + # ---- head ---- + hx0 = cx + dx_sh + head_dx - head_w/2 + out.append(part("head", head(hx0, head_y, head_w, head_h, skin, hview, eyes, mouth_kind, shadow, hair, uid + "h", human_ears))) + for acc in accessories: + out.append(part("accessory", accessory(acc, hx0, head_y, head_w, head_h, cx + dx_sh, y0, sw, hview))) + if over: + out.append(part("over", over(g))) + return f'' + "\n".join(out) + "" if TAGS else "\n".join(out) + +def accessory(kind, hx, hy, hw, hh, scx, sy0, sw, view): + """small identity props: glasses | scarf | bow | badge""" + ey = hy + hh/3 + if kind == "glasses" and view in ("front", "q", "qr"): + g = hw*0.36; r = hw*0.115 + if view == "front": xs = (hx+hw/2-g/2, hx+hw/2+g/2) + else: xs = (hx+hw*0.6, hx+hw*0.2) if view == "q" else (hx+hw*0.4, hx+hw*0.8) + return line(ellipse_path(xs[0], ey, r, r*0.85)) + line(ellipse_path(xs[1], ey, r if view == "front" else r*0.7, r*0.85)) + line(f"M{min(xs)+r*0.9:.1f} {ey:.1f} L{max(xs)-r*0.9:.1f} {ey:.1f}") + if kind == "scarf": + return outline(ellipse_path(scx, sy0 + sw*0.2, sw*0.46, sw*0.14), PINK) + outline(f"M{scx+hw*0.2:.1f} {sy0+sw*0.26:.1f} C{scx+hw*0.5:.1f} {sy0+sw*0.6:.1f} {scx+hw*0.5:.1f} {sy0+sw*0.9:.1f} {scx+hw*0.3:.1f} {sy0+sw*1.1:.1f} C{scx+hw*0.15:.1f} {sy0+sw*0.9:.1f} {scx+hw*0.1:.1f} {sy0+sw*0.6:.1f} {scx+hw*0.2:.1f} {sy0+sw*0.26:.1f} Z", PINK) + if kind == "bow": + bx, by = hx + hw*0.78, hy - hh*0.02 + return outline(f"M{bx:.1f} {by:.1f} C{bx-hw*0.3:.1f} {by-hh*0.18:.1f} {bx-hw*0.3:.1f} {by+hh*0.1:.1f} {bx:.1f} {by:.1f} C{bx+hw*0.3:.1f} {by-hh*0.18:.1f} {bx+hw*0.3:.1f} {by+hh*0.1:.1f} {bx:.1f} {by:.1f} Z", YEL) + blob(bx, by, hw*0.06, hw*0.06, YEL) + if kind == "badge": + return blob(scx - sw*0.22, sy0 + sw*0.55, sw*0.1, sw*0.1, YEL) + return "" diff --git a/ventures/househand/index.html b/ventures/househand/index.html new file mode 100644 index 0000000..627d1e2 --- /dev/null +++ b/ventures/househand/index.html @@ -0,0 +1,200 @@ + + + + + +House Hand + + + + + + + + + + + + + + + + +
+
+ + + + + +
+

house hand

+

A drawing hand for agents. Characters, mascots and scenes in one consistent style, drawn in Python and handed back as SVG, PNG, or a drawing that draws itself.

+ +
+
+
+

Use it with your agent

+

Copy this prompt into any agent that can run Python. It reads the skill, draws with house.py, and hands +back the files.

+
Draw a gardener with a watering can, waving hello, in the House Hand style.
+
+First read https://househand.yolopreneur.lol/skill.md and follow it. Download house.py from the same site and draw with it in Python, keeping its rules: three fingers and a thumb, four toes, limbs as round tubes, one ink line weight, flat colours from its palette.
+
+Give me the SVG and a PNG. Then run drawon.py on the SVG so I also get a version that draws itself.
+
Change +the first line to what you want drawn.
+

What comes back

+
  • SVGThe drawing as one file: flat colours, one ink line, nothing linked from outside.
  • PNGRendered from the SVG at whatever size you need.
  • A drawing that draws itselfdrawon.py replays the drawing stroke by stroke, head first. It plays in any browser, and in an ordinals explorer once inscribed.
  • VideoAn MP4 or GIF of the drawing being made, for posts and previews.
+

Coming next: an API and an MCP server, so an agent can ask for a drawing and get these files back +without running any code, and Lottie files for apps.

+

The files: skill.md (how to draw in the hand), house.py (the drawing library), drawon.py (the replay), llms.txt (this list, for agents).

+
+

Plates

32 drawings, in the order they +were drawn. Each one draws itself when you scroll to it. Click a thumbnail to jump there.

+
  1. 1Inflated cube
  2. 2Smooth bean
  3. 3Tube discipline
  4. 4Reduce by half
  5. 5Edgy, not eggy
  6. 6The face grid
  7. 7Expressions
  8. 8Head turnaround, five views
  9. 9Five bodies
  10. 10Noodle limbs
  11. 11The hand library
  12. 12Feet and footwear
  13. 13Cuffs and lips
  14. 14Saturated shadows
  15. 15Strokeless matter
  16. 16Attitude line-up
  17. 17Pose sheet, twelve up
  18. 18Full turnaround, eight views and a hero
  19. 19Depth without blur
  20. 20Dark-key inversion
  21. 21Twelve-frame loop, contact sheet
  22. 22The loop itself
  23. 23Phly's character sheet
  24. 24Phly, hero
  25. 25Phly's mark
  26. 26Phly on the mug
  27. 27The tea garden
  28. 28The tea garden assembles
  29. 29Bunny
  30. 30Unicorn
  31. 31Bunny and Unicorn dance
  32. 32The dance loop
+

How the drawings were made

The studio had a published style guide it loved and no hand that could draw in it. So the guide was read front to back and turned into twenty-one katas, about four to a belt, each with pass conditions written as things a reviewer could see. Every drawing on this page comes out of one Python file, house.py, which grew a function per belt: head, bean, tubes, hands, feet, garments, shadows, FX, and finally figure().

Two graders, and neither of them draws. svg_lint.py checks the mechanical rules on the SVG itself: one line weight, round caps and joins, palette colours only, no gradients or filters. Cloudflare's Clef model looks at the rendered picture and answers each rule as a probability. A sheet clears its kata at 1.8 out of 3 with no single check under 0.4. When a sheet stalled, the move was to ask the grader one claim at a time and fix whichever came back weakest, never to argue with the number.

The plates replay their own making. drawon.py traces every ink line at one steady pen speed, lets each shape fill with paper as it closes, then floods in the flat colours, the shadows and the effects. house.py marks the parts of each character as it draws them, so the replay goes the way an artist works: head first, feet last.

+

White belt

One form at a time

Four katas on single forms: the inflated cube, the smooth bean, tube discipline, reduce-by-half. This is where the lint grader was written: constant stroke width, round caps and joins, palette-only fills, no gradients or filters. Every later sheet has to pass it before a reviewer ever sees it.

The first lesson was cheap and permanent: the producer never grades its own work. The lint is deterministic; the visual judgement goes to a separate model.

Inflated cube. Corners 9% of edge, every edge bowed, interior lines stopping short of the centre.

1Inflated cube

Corners 9% of edge, every edge bowed, interior lines stopping short of the centre.

Kata 1

Smooth bean. Seven anchors, one arced back, a belly circle at ~60% of the height.

2Smooth bean

Seven anchors, one arced back, a belly circle at ~60% of the height.

Kata 2

Tube discipline. Constant-width tubes, loop radius never below the width, round ends.

3Tube discipline

Constant-width tubes, loop radius never below the width, round ends.

Kata 3

Reduce by half. The same object at half the line count; features deleted, not shrunk.

4Reduce by half

The same object at half the line count; features deleted, not shrunk.

Kata 4

Yellow belt

The head

The head is an oblong, not an egg: 1.2× taller than wide, flattened top and bottom. Eyes sit on the upper third, the mouth just below them. Kata 8 (the turnaround) took seven attempts; the breakthrough came from asking the grader one claim at a time — closed-lobe ears and a compressed far eye lifted it from 1.77 to 1.80.

Edgy, not eggy.

5Edgy, not eggy

Kata 5, Clef 2.07

The face grid.

6The face grid

Kata 6, Clef 1.85

Expressions. Only the eyes and mouth change.

7Expressions

Only the eyes and mouth change.

Kata 7, Clef 2.08

Head turnaround, five views.

8Head turnaround, five views

Kata 8, Clef 1.80

Green belt

Bodies, limbs, hands, feet

Beans for torsos, tubes for limbs, and the two house rules the creative director set over the field guide: hands are three fingers plus a thumb (thumb 1.15×, not oversized), feet have four toes. Kata 9 needed the bean on the hip line and shoulders at 0.72× before it cleared.

Five bodies.

9Five bodies

Kata 9, Clef 1.85

Noodle limbs. Constant width, single curves, no elbows.

10Noodle limbs

Constant width, single curves, no elbows.

Kata 10, Clef 2.17

The hand library.

11The hand library

Kata 11, Clef 2.06

Feet and footwear.

12Feet and footwear

Kata 12, Clef 2.01

Blue belt

Garments, shadows, matter

Every opening is an ellipse with a lip; sleeves and trouser legs are 1.6–1.7× the limb. Every shadow is a more saturated version of its base hue, one hard-edged strokeless shape per form. FX — steam, sparkles, liquid, fire — carry no outline at all. The lint learned the saturated-shadow rule here (hue ±5°, saturation up, value down by at most 0.12).

Cuffs and lips.

13Cuffs and lips

Kata 13, Clef 1.94

Saturated shadows.

14Saturated shadows

Kata 14, Clef 2.27

Strokeless matter.

15Strokeless matter

Kata 15, Clef 2.09

Brown belt

Whole figures

The full-figure taste test. One library call, figure(), draws an on-model character from a view, a lean, arm and leg targets, hands, props and accessories. Kata 17 cleared once the sitting pose actually sat (hips at bench height); kata 18 cleared once the figures were drawn big enough for the grader to see cuffs and shadows at all — a rule the dojo now keeps: nothing under about 70px of head height on a graded sheet.

Attitude line-up. Four characters, one ground shadow.

16Attitude line-up

Four characters, one ground shadow.

Kata 16, Clef 1.83

Pose sheet, twelve up.

17Pose sheet, twelve up

Kata 17, Clef 1.83

Full turnaround, eight views and a hero.

18Full turnaround, eight views and a hero

Kata 18, Clef 1.81

Black belt

Depth, dark key, motion

Depth without blur: overlap, scale and value only. The dark-key inversion swaps the shadow family in as the base and lights the scene with a strokeless halo. The twelve-frame loop exposed a real bug — figure() had a fixed ink width, so a half-scale figure drew with double-weight line — and taught the reviewer-facing trick of putting the seam on the sheet: frames 1 and 2 repeated, ghosted, after 12.

Depth without blur.

19Depth without blur

Kata 19, Clef 1.94

Dark-key inversion.

20Dark-key inversion

Kata 20, Clef 1.91

Twelve-frame loop, contact sheet.

21Twelve-frame loop, contact sheet

Kata 21, Clef 1.85

The loop itself. Twelve frames, a second long.

22The loop itself

Twelve frames, a second long.

After the ladder

Phly, the mug, the garden

With the hand locked in, the real work: the phly.cloud mascot rebuilt as one closed silhouette with wings, cap, goggles and scarf (2.00 first time); the old mug scene redrawn with figure(); and the maths.teahub.xyz title card remade as a frame-by-frame animation from the same drawing library.

Two of those three never cleared the tangent check (0.36 against a 0.4 gate). The probe named the same thing every time: a figure or object resting on a horizontal set line. That's the rule the next version of the dojo gets — break the line with a clear overlap, or lift the thing off it.

Phly's character sheet. Hero, three expressions and the mark.

23Phly's character sheet

Hero, three expressions and the mark.

Clef 2.00

Phly, hero.

24Phly, hero

Phly's mark. Drawn to work as a favicon.

25Phly's mark

Drawn to work as a favicon.

Phly on the mug. The old scene, house hand. Tangents 0.36.

26Phly on the mug

The old scene, house hand. Tangents 0.36.

The tea garden. End frame. Tangents 0.36.

27The tea garden

End frame. Tangents 0.36.

The tea garden assembles. Nine seconds, ending on the frame above.

28The tea garden assembles

Nine seconds, ending on the frame above.

New cast

Bunny, Unicorn, and a dance

Two new cast members, each with a card of her own: Bunny (kata 23) — long lilac ears, the right one flopping, a pink dress with puff sleeves and a cotton tail — and Unicorn (kata 24) — a gold spiral horn, pony ears, a lavender mane with one pink streak, white hoof boots. The library grew a little for each: hooks to draw parts behind, between and over the body, a palm that stays wrist-wide on big hands, and opt-in fixes for small figures, where the constant 10px line closes cuffs, soles and collars up into solid ink.

Bunny cleared at 1.81. Unicorn plateaued at 1.65 over seven grades and is committed ungated, with her scores. Then the two of them got a scene. The first version held hands, and every clasp — fingers under a fist, one fist hiding both hands — came back as the worst tangent on the page; a high five with a clear gap passed every check.

Bunny. Hero, turnaround, ear moods, colour chips and the mark.

29Bunny

Hero, turnaround, ear moods, colour chips and the mark.

Clef 1.81

Unicorn. Hero, turnaround, moods carried by the ears and the horn's sparkles, the mark. Ungated (gate 1.8).

30Unicorn

Hero, turnaround, moods carried by the ears and the horn's sparkles, the mark. Ungated (gate 1.8).

Clef 1.65

Bunny and Unicorn dance. The still. Every check at least 0.4; tangents 0.43.

31Bunny and Unicorn dance

The still. Every check at least 0.4; tangents 0.43.

The dance loop. Two seconds: two hops, and a clap on the second landing.

32The dance loop

Two seconds: two hops, and a clap on the second landing.

+
+

House Hand is a venture on yolopreneur.lol. Every drawing here was drawn +by Claude in Python with house.py and graded by svg_lint and Cloudflare's Clef, October 2026.

+

Headings are set in Fredoka, under the SIL Open Font License.

+
+ + + diff --git a/ventures/househand/llms.txt b/ventures/househand/llms.txt new file mode 100644 index 0000000..dc31798 --- /dev/null +++ b/ventures/househand/llms.txt @@ -0,0 +1,20 @@ +# House Hand + +> A drawing hand for agents: characters, mascots, icons and scenes in one consistent style, drawn in Python with house.py and handed back as SVG, PNG, or a drawing that draws itself. + +Read skill.md first and follow it. house.py and drawon.py need only Python 3.9 or later and its standard library. + +## Files + +- [skill.md](https://househand.yolopreneur.lol/skill.md): how to draw in the House Hand: the rules, the library and the workflow +- [house.py](https://househand.yolopreneur.lol/house.py): the drawing library; `figure()` draws a whole character +- [drawon.py](https://househand.yolopreneur.lol/drawon.py): turns a finished SVG into one that draws itself, stroke by stroke + +## Examples + +- [Plates](https://househand.yolopreneur.lol/#plates): every drawing made with the library so far, from the first kata to the current cast, each one drawing itself + +## Coming next + +- An API and an MCP server that take a description and return the files, without running any code +- Lottie output for apps diff --git a/ventures/househand/plates/01-inflated-cube.svg b/ventures/househand/plates/01-inflated-cube.svg new file mode 100644 index 0000000..267edb8 --- /dev/null +++ b/ventures/househand/plates/01-inflated-cube.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ventures/househand/plates/01-inflated-cube.webp b/ventures/househand/plates/01-inflated-cube.webp new file mode 100644 index 0000000..0d4cd04 Binary files /dev/null and b/ventures/househand/plates/01-inflated-cube.webp differ diff --git a/ventures/househand/plates/02-smooth-bean.svg b/ventures/househand/plates/02-smooth-bean.svg new file mode 100644 index 0000000..22153cb --- /dev/null +++ b/ventures/househand/plates/02-smooth-bean.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ventures/househand/plates/02-smooth-bean.webp b/ventures/househand/plates/02-smooth-bean.webp new file mode 100644 index 0000000..889ffd3 Binary files /dev/null and b/ventures/househand/plates/02-smooth-bean.webp differ diff --git a/ventures/househand/plates/03-tube-discipline.svg b/ventures/househand/plates/03-tube-discipline.svg new file mode 100644 index 0000000..9c59b39 --- /dev/null +++ b/ventures/househand/plates/03-tube-discipline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ventures/househand/plates/03-tube-discipline.webp b/ventures/househand/plates/03-tube-discipline.webp new file mode 100644 index 0000000..323b54f Binary files /dev/null and b/ventures/househand/plates/03-tube-discipline.webp differ diff --git a/ventures/househand/plates/04-reduce-by-half.svg b/ventures/househand/plates/04-reduce-by-half.svg new file mode 100644 index 0000000..633975f --- /dev/null +++ b/ventures/househand/plates/04-reduce-by-half.svg @@ -0,0 +1,35 @@ + + + detailed · 24 interior lines + reduced · 9 interior lines + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ventures/househand/plates/04-reduce-by-half.webp b/ventures/househand/plates/04-reduce-by-half.webp new file mode 100644 index 0000000..8d4e5c7 Binary files /dev/null and b/ventures/househand/plates/04-reduce-by-half.webp differ diff --git a/ventures/househand/plates/05-edgy-not-eggy.svg b/ventures/househand/plates/05-edgy-not-eggy.svg new file mode 100644 index 0000000..609b75f --- /dev/null +++ b/ventures/househand/plates/05-edgy-not-eggy.svg @@ -0,0 +1,22 @@ + + +Kata 5 — Edgy, not eggy: six blank heads, 1.2× taller than wide, top and bottom flattened + + +75×90 + + +108×130 + + +150×180 + + +200×240 + + +250×300 + + +300×360 + \ No newline at end of file diff --git a/ventures/househand/plates/05-edgy-not-eggy.webp b/ventures/househand/plates/05-edgy-not-eggy.webp new file mode 100644 index 0000000..79fb102 Binary files /dev/null and b/ventures/househand/plates/05-edgy-not-eggy.webp differ diff --git a/ventures/househand/plates/06-face-grid.svg b/ventures/househand/plates/06-face-grid.svg new file mode 100644 index 0000000..8c1d341 --- /dev/null +++ b/ventures/househand/plates/06-face-grid.svg @@ -0,0 +1,70 @@ + + +Kata 6 — Face grid: the right face, and eight single-variable mistakes + + + + + + +✓ ours + + + + + + +✗ eyes too low (centre) + + + + + + +✗ eyes too high + + + + + + +✗ mouth too low + + + + + + +✗ mouth off-centre + + + + + + +✗ eyes too close + + + + + + +✗ eyes too far (edges) + + + + + + +✗ eyes too big + + + + + + +✗ eyes too small +eye line at ⅓ · mouth +10% of height +mouth width ≈ 0.9 × eye gap +one variable wrong per head + \ No newline at end of file diff --git a/ventures/househand/plates/06-face-grid.webp b/ventures/househand/plates/06-face-grid.webp new file mode 100644 index 0000000..1b113d0 Binary files /dev/null and b/ventures/househand/plates/06-face-grid.webp differ diff --git a/ventures/househand/plates/07-expressions.svg b/ventures/househand/plates/07-expressions.svg new file mode 100644 index 0000000..406cc59 --- /dev/null +++ b/ventures/househand/plates/07-expressions.svg @@ -0,0 +1,136 @@ + + +Kata 7 — Sixteen expressions on one head + + + + + + + +neutral + + + + + + + +happy + + + + + + + + +delighted + + + + + + + +laughing + + + + + + + +sad + + + + + + + + +crying + + + + + + + +surprised + + + + + + + +shocked + + + + + + + +bored + + + + + + + +sleepy + + + + + + + +smug + + + + + + + +cheeky + + + + + + + + +shy + + + + + + + +queasy + + + + + + + +focused + + + + + + + +content +same head, same tuft, same line. only the feature SHAPES change — never their positions. + \ No newline at end of file diff --git a/ventures/househand/plates/07-expressions.webp b/ventures/househand/plates/07-expressions.webp new file mode 100644 index 0000000..1ee4470 Binary files /dev/null and b/ventures/househand/plates/07-expressions.webp differ diff --git a/ventures/househand/plates/08-turnaround.svg b/ventures/househand/plates/08-turnaround.svg new file mode 100644 index 0000000..2d6ee34 --- /dev/null +++ b/ventures/househand/plates/08-turnaround.svg @@ -0,0 +1,71 @@ + + +Kata 8 — Turnaround: eight views of one head + +top + +eyes + +mouth + +chin + +top + +eyes + +mouth + +chin + + + + + + + + +front + + + + + + + +¾ + + + + + +side + + + +¾ back + + + + +back + + + +¾ back + + + + + +side + + + + + + + +¾ +height held · width narrows ~20% in profile · half a mouth in profile · far eye compresses but stays inside · ears walk toward centre · back carries nothing + \ No newline at end of file diff --git a/ventures/househand/plates/08-turnaround.webp b/ventures/househand/plates/08-turnaround.webp new file mode 100644 index 0000000..48194a3 Binary files /dev/null and b/ventures/househand/plates/08-turnaround.webp differ diff --git a/ventures/househand/plates/09-five-bodies.svg b/ventures/househand/plates/09-five-bodies.svg new file mode 100644 index 0000000..30f16d8 --- /dev/null +++ b/ventures/househand/plates/09-five-bodies.svg @@ -0,0 +1,125 @@ + + +Kata 9 — Five bodies, one bean +slim · heavy · short · tall · child — same construction, different circle and extrusion + + + + + + + + + + + + + + + + + + + + + + +slim +4.75 heads + + + + + + + + + + + + + + + + + + + + + + +heavy +4.6 heads · head +12% + + + + + + + + + + + + + + + + + + + + + + +short +4.5 heads + + + + + + + + + + + + + + + + + + + + + + +tall +5 heads + + + + + + + + + + + + + + + + + + + + + +child +3.3 heads · same head +bean = belly circle (pink) extruded up · shoulders ≤ belly · legs = ½ height · head size held, torso doubles + \ No newline at end of file diff --git a/ventures/househand/plates/09-five-bodies.webp b/ventures/househand/plates/09-five-bodies.webp new file mode 100644 index 0000000..6502b9f Binary files /dev/null and b/ventures/househand/plates/09-five-bodies.webp differ diff --git a/ventures/househand/plates/10-noodle-limbs.svg b/ventures/househand/plates/10-noodle-limbs.svg new file mode 100644 index 0000000..9fd1fc0 --- /dev/null +++ b/ventures/househand/plates/10-noodle-limbs.svg @@ -0,0 +1,29 @@ + + +Kata 10 — Noodle limbs +eight tubes, one curve each, 45°–120° · constant width · no elbow, no knee, no taper · inner radius ≥ 0.3× width + + +arm · 45° + + +arm · 70° + + +arm · 95° + + +arm · 120° + + +leg · 50° + + +leg · 80° + + +leg · 100° + + +leg · 120° + \ No newline at end of file diff --git a/ventures/househand/plates/10-noodle-limbs.webp b/ventures/househand/plates/10-noodle-limbs.webp new file mode 100644 index 0000000..40f73a2 Binary files /dev/null and b/ventures/househand/plates/10-noodle-limbs.webp differ diff --git a/ventures/househand/plates/11-hand-library.svg b/ventures/househand/plates/11-hand-library.svg new file mode 100644 index 0000000..7c739b0 --- /dev/null +++ b/ventures/househand/plates/11-hand-library.svg @@ -0,0 +1,77 @@ + + +Kata 11 — Hand library +fingers = round-tipped tubes, no nails, no creases · thumb 1.3× and shorter, low on the palm · wrist never narrows · prop over the far fingers, under the near ones + + + + +open palm + + + + +point + + + + +fist + + + + +peace + + + + +pinch + + + + +thumbs-up + + + + + + +grip · wand + + + + + + +grip · cup + + + + + + +grip · ball + + + + + + +grip · pencil + + + + + + +grip · phone + + + + + + +grip · flag + \ No newline at end of file diff --git a/ventures/househand/plates/11-hand-library.webp b/ventures/househand/plates/11-hand-library.webp new file mode 100644 index 0000000..18794e8 Binary files /dev/null and b/ventures/househand/plates/11-hand-library.webp differ diff --git a/ventures/househand/plates/12-feet-footwear.svg b/ventures/househand/plates/12-feet-footwear.svg new file mode 100644 index 0000000..2a08077 --- /dev/null +++ b/ventures/househand/plates/12-feet-footwear.svg @@ -0,0 +1,35 @@ + + +Kata 12 — Feet & footwear +five round-lobed toes, big toe 1.6× · ankle = continuous tube · shoe = upper + sole band 15–20%, thicker at the heel, bowed · ≤ 3 interior lines · feet bend + + + + + + + +bare · side + + + + + + + +bare · front + + + + + + + +bare · ¾ + +sneaker + +boot + +bent · push-off + \ No newline at end of file diff --git a/ventures/househand/plates/12-feet-footwear.webp b/ventures/househand/plates/12-feet-footwear.webp new file mode 100644 index 0000000..b9f2954 Binary files /dev/null and b/ventures/househand/plates/12-feet-footwear.webp differ diff --git a/ventures/househand/plates/13-cuffs-lips.svg b/ventures/househand/plates/13-cuffs-lips.svg new file mode 100644 index 0000000..7fbadc4 --- /dev/null +++ b/ventures/househand/plates/13-cuffs-lips.svg @@ -0,0 +1,68 @@ + + +Kata 13 — Cuffs & lips +same figure twice · sleeves and trouser legs hold 1.5–2× the limb · cuff, hem, collar = ellipse + lip · ≤ 2 folds per garment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +✗ garments as skins — colour swapped on the limb, no openings +✓ garments as volume — 1.7× the limb, every opening an ellipse with a lip + \ No newline at end of file diff --git a/ventures/househand/plates/13-cuffs-lips.webp b/ventures/househand/plates/13-cuffs-lips.webp new file mode 100644 index 0000000..3350a62 Binary files /dev/null and b/ventures/househand/plates/13-cuffs-lips.webp differ diff --git a/ventures/househand/plates/14-saturated-shadows.svg b/ventures/househand/plates/14-saturated-shadows.svg new file mode 100644 index 0000000..2377a18 --- /dev/null +++ b/ventures/househand/plates/14-saturated-shadows.svg @@ -0,0 +1,59 @@ + + +Kata 14 — Saturated shadows +six objects, six hues · base and shadow swatched with hex values + + + + +mug + + + +ball + + + +book + + + +pot + + + +pear + + +egg + + +#FFC897 +#E69248 +sat +28 val -10% hue Δ0° + + +#FFB3E6 +#E661BA +sat +28 val -10% hue Δ0° + + +#A1E9FF +#51C3E6 +sat +28 val -10% hue Δ0° + + +#B3FFC7 +#61E684 +sat +28 val -10% hue Δ0° + + +#FFE780 +#E6C433 +sat +28 val -10% hue Δ0° + + +#FFFFFF +#C5C5FF @50% +shadow = same hue, saturation up, value down ≤ 20% · one hard-edged strokeless shape per form, ending on the outline · white things get a low-opacity purple + \ No newline at end of file diff --git a/ventures/househand/plates/14-saturated-shadows.webp b/ventures/househand/plates/14-saturated-shadows.webp new file mode 100644 index 0000000..dcee09c Binary files /dev/null and b/ventures/househand/plates/14-saturated-shadows.webp differ diff --git a/ventures/househand/plates/15-strokeless-matter.svg b/ventures/househand/plates/15-strokeless-matter.svg new file mode 100644 index 0000000..b865e21 --- /dev/null +++ b/ventures/househand/plates/15-strokeless-matter.svg @@ -0,0 +1,54 @@ + + +Kata 15 — Strokeless matter +eight FX, not one outline · each a flat base plus at least one value or highlight shape + +splash + + + + +smoke + +fire + + + + +drip · stain + + + + + + + + + + +sparkles + + + + + + + + + + + +sweat + + + + + +glow + + + + + +goo + \ No newline at end of file diff --git a/ventures/househand/plates/15-strokeless-matter.webp b/ventures/househand/plates/15-strokeless-matter.webp new file mode 100644 index 0000000..a7d483d Binary files /dev/null and b/ventures/househand/plates/15-strokeless-matter.webp differ diff --git a/ventures/househand/plates/16-attitude-lineup.svg b/ventures/househand/plates/16-attitude-lineup.svg new file mode 100644 index 0000000..12a7a16 --- /dev/null +++ b/ventures/househand/plates/16-attitude-lineup.svg @@ -0,0 +1,143 @@ + + +Kata 16 — Attitude line-up +four characters, one ground shadow · same head, bean, limb widths · identity by hair mass + accessory · attitude by lean, tilt, arms + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +leader +sceptic +enthusiast +sleepy + \ No newline at end of file diff --git a/ventures/househand/plates/16-attitude-lineup.webp b/ventures/househand/plates/16-attitude-lineup.webp new file mode 100644 index 0000000..d6934d4 Binary files /dev/null and b/ventures/househand/plates/16-attitude-lineup.webp differ diff --git a/ventures/househand/plates/17-pose-sheet.svg b/ventures/househand/plates/17-pose-sheet.svg new file mode 100644 index 0000000..e2fbbe6 --- /dev/null +++ b/ventures/househand/plates/17-pose-sheet.svg @@ -0,0 +1,404 @@ + + +Kata 17 — Pose sheet, 12 up +one character · a line of action through head → back arc → leading limb · limbs curve with the pose · foreshortening by overlap and scale (reaching, pointing: hand 1.3×) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +sitting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +lying + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +running + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +jumping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +falling + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +crouching + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +reaching + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +leaning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +waving + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +pointing + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +thinking + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +dancing + \ No newline at end of file diff --git a/ventures/househand/plates/17-pose-sheet.webp b/ventures/househand/plates/17-pose-sheet.webp new file mode 100644 index 0000000..41a22de Binary files /dev/null and b/ventures/househand/plates/17-pose-sheet.webp differ diff --git a/ventures/househand/plates/18-full-turnaround.svg b/ventures/househand/plates/18-full-turnaround.svg new file mode 100644 index 0000000..857db8a --- /dev/null +++ b/ventures/househand/plates/18-full-turnaround.svg @@ -0,0 +1,320 @@ + + +Kata 18 — Full turnaround · eight views, 360°, coloured and shaded, then one hero pose + + +top + +chin + +hem + +ground + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +front + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +¾ + + + + + + + + + + + + + + + + + + + + + + + + + + +side + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +¾ back + +top + +chin + +hem + +ground + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +back + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +¾ back + + + + + + + + + + + + + + + + + + + + + + + + + + +side + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +¾ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +hero +height, head size, hairline, +hem, shoe size, limb widths +held across all eight views · +profile 0.78 wide · back: no face, +one fold · shadows on the right + \ No newline at end of file diff --git a/ventures/househand/plates/18-full-turnaround.webp b/ventures/househand/plates/18-full-turnaround.webp new file mode 100644 index 0000000..ddc9b8c Binary files /dev/null and b/ventures/househand/plates/18-full-turnaround.webp differ diff --git a/ventures/househand/plates/19-depth-without-blur.svg b/ventures/househand/plates/19-depth-without-blur.svg new file mode 100644 index 0000000..acb3107 --- /dev/null +++ b/ventures/househand/plates/19-depth-without-blur.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Kata 19 — Depth without blur + + + \ No newline at end of file diff --git a/ventures/househand/plates/19-depth-without-blur.webp b/ventures/househand/plates/19-depth-without-blur.webp new file mode 100644 index 0000000..f96feca Binary files /dev/null and b/ventures/househand/plates/19-depth-without-blur.webp differ diff --git a/ventures/househand/plates/20-dark-key.svg b/ventures/househand/plates/20-dark-key.svg new file mode 100644 index 0000000..5aed444 --- /dev/null +++ b/ventures/househand/plates/20-dark-key.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Kata 20 — Dark-key inversion · light pastel key +dark key · shadow colours as base · one accent hue · strokeless halo + + + \ No newline at end of file diff --git a/ventures/househand/plates/20-dark-key.webp b/ventures/househand/plates/20-dark-key.webp new file mode 100644 index 0000000..c0e82cc Binary files /dev/null and b/ventures/househand/plates/20-dark-key.webp differ diff --git a/ventures/househand/plates/21-twelve-frame-loop.svg b/ventures/househand/plates/21-twelve-frame-loop.svg new file mode 100644 index 0000000..2acc785 --- /dev/null +++ b/ventures/househand/plates/21-twelve-frame-loop.svg @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +11 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1 again (loop) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +2 again (loop) +Kata 21 — twelve-frame loop · 1→12, then 12 flows back into 1 (ghosted) · bounce + lean + wave are sinusoidal over the loop · sparkles pulse, the third fades in and out + + + \ No newline at end of file diff --git a/ventures/househand/plates/21-twelve-frame-loop.webp b/ventures/househand/plates/21-twelve-frame-loop.webp new file mode 100644 index 0000000..e769942 Binary files /dev/null and b/ventures/househand/plates/21-twelve-frame-loop.webp differ diff --git a/ventures/househand/plates/bunny-sheet.svg b/ventures/househand/plates/bunny-sheet.svg new file mode 100644 index 0000000..a932e4f --- /dev/null +++ b/ventures/househand/plates/bunny-sheet.svg @@ -0,0 +1,221 @@ + + +Bunny — new cast member, house hand +the ears carry the mood · her right ear flops +bob with a fringe · pink dress, puff sleeves, hem lip +cotton tail · yellow shoes and bow · carrot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +front + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +¾ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +side + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +back + + + + + +happy · one up, one flops + + + + + +surprised · both up + + + + + +sad · both hang + + + + + +sleepy · both droop + + + + +mark · eyes only + +lilac · hair, ears + +pink · dress + +white · tail, tights + +yellow · shoes, bow + +orange · carrot + +mint · leaves + \ No newline at end of file diff --git a/ventures/househand/plates/bunny-sheet.webp b/ventures/househand/plates/bunny-sheet.webp new file mode 100644 index 0000000..fcbdde1 Binary files /dev/null and b/ventures/househand/plates/bunny-sheet.webp differ diff --git a/ventures/househand/plates/dance-still.svg b/ventures/househand/plates/dance-still.svg new file mode 100644 index 0000000..0f5fc15 --- /dev/null +++ b/ventures/househand/plates/dance-still.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ventures/househand/plates/dance-still.webp b/ventures/househand/plates/dance-still.webp new file mode 100644 index 0000000..ed75aec Binary files /dev/null and b/ventures/househand/plates/dance-still.webp differ diff --git a/ventures/househand/plates/dance.gif b/ventures/househand/plates/dance.gif new file mode 100644 index 0000000..6918f8c Binary files /dev/null and b/ventures/househand/plates/dance.gif differ diff --git a/ventures/househand/plates/garden-still.svg b/ventures/househand/plates/garden-still.svg new file mode 100644 index 0000000..7906fc4 --- /dev/null +++ b/ventures/househand/plates/garden-still.svg @@ -0,0 +1 @@ +have your tea & drink it too \ No newline at end of file diff --git a/ventures/househand/plates/garden-still.webp b/ventures/househand/plates/garden-still.webp new file mode 100644 index 0000000..ff4b909 Binary files /dev/null and b/ventures/househand/plates/garden-still.webp differ diff --git a/ventures/househand/plates/garden.gif b/ventures/househand/plates/garden.gif new file mode 100644 index 0000000..09d3ee8 Binary files /dev/null and b/ventures/househand/plates/garden.gif differ diff --git a/ventures/househand/plates/loop.gif b/ventures/househand/plates/loop.gif new file mode 100644 index 0000000..ae24aa8 Binary files /dev/null and b/ventures/househand/plates/loop.gif differ diff --git a/ventures/househand/plates/phly-hero.svg b/ventures/househand/plates/phly-hero.svg new file mode 100644 index 0000000..ed3af37 --- /dev/null +++ b/ventures/househand/plates/phly-hero.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ventures/househand/plates/phly-hero.webp b/ventures/househand/plates/phly-hero.webp new file mode 100644 index 0000000..e6d93c6 Binary files /dev/null and b/ventures/househand/plates/phly-hero.webp differ diff --git a/ventures/househand/plates/phly-mark.svg b/ventures/househand/plates/phly-mark.svg new file mode 100644 index 0000000..1758b95 --- /dev/null +++ b/ventures/househand/plates/phly-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ventures/househand/plates/phly-mark.webp b/ventures/househand/plates/phly-mark.webp new file mode 100644 index 0000000..fd92d9d Binary files /dev/null and b/ventures/househand/plates/phly-mark.webp differ diff --git a/ventures/househand/plates/phly-mug.svg b/ventures/househand/plates/phly-mug.svg new file mode 100644 index 0000000..00d04ef --- /dev/null +++ b/ventures/househand/plates/phly-mug.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Phly on the mug — redrawn in the house hand + \ No newline at end of file diff --git a/ventures/househand/plates/phly-mug.webp b/ventures/househand/plates/phly-mug.webp new file mode 100644 index 0000000..04ced89 Binary files /dev/null and b/ventures/househand/plates/phly-mug.webp differ diff --git a/ventures/househand/plates/phly-sheet.svg b/ventures/househand/plates/phly-sheet.svg new file mode 100644 index 0000000..24f30ce --- /dev/null +++ b/ventures/househand/plates/phly-sheet.svg @@ -0,0 +1,22 @@ + + +Phly — the phly.cloud mascot, in the house hand +a cloud with fly wings · aviator cap + goggles · scarf · one saturated shadow per form · wings and ticks as strokeless-feel tints + + + + + + + + + + +excited + +chill + +surprised + +mark · cloud, wings, eyes only + \ No newline at end of file diff --git a/ventures/househand/plates/phly-sheet.webp b/ventures/househand/plates/phly-sheet.webp new file mode 100644 index 0000000..75dc669 Binary files /dev/null and b/ventures/househand/plates/phly-sheet.webp differ diff --git a/ventures/househand/plates/thumbs/01-inflated-cube.webp b/ventures/househand/plates/thumbs/01-inflated-cube.webp new file mode 100644 index 0000000..60edb60 Binary files /dev/null and b/ventures/househand/plates/thumbs/01-inflated-cube.webp differ diff --git a/ventures/househand/plates/thumbs/02-smooth-bean.webp b/ventures/househand/plates/thumbs/02-smooth-bean.webp new file mode 100644 index 0000000..c42da60 Binary files /dev/null and b/ventures/househand/plates/thumbs/02-smooth-bean.webp differ diff --git a/ventures/househand/plates/thumbs/03-tube-discipline.webp b/ventures/househand/plates/thumbs/03-tube-discipline.webp new file mode 100644 index 0000000..e79c3c4 Binary files /dev/null and b/ventures/househand/plates/thumbs/03-tube-discipline.webp differ diff --git a/ventures/househand/plates/thumbs/04-reduce-by-half.webp b/ventures/househand/plates/thumbs/04-reduce-by-half.webp new file mode 100644 index 0000000..80855b1 Binary files /dev/null and b/ventures/househand/plates/thumbs/04-reduce-by-half.webp differ diff --git a/ventures/househand/plates/thumbs/05-edgy-not-eggy.webp b/ventures/househand/plates/thumbs/05-edgy-not-eggy.webp new file mode 100644 index 0000000..4154a53 Binary files /dev/null and b/ventures/househand/plates/thumbs/05-edgy-not-eggy.webp differ diff --git a/ventures/househand/plates/thumbs/06-face-grid.webp b/ventures/househand/plates/thumbs/06-face-grid.webp new file mode 100644 index 0000000..dd8763b Binary files /dev/null and b/ventures/househand/plates/thumbs/06-face-grid.webp differ diff --git a/ventures/househand/plates/thumbs/07-expressions.webp b/ventures/househand/plates/thumbs/07-expressions.webp new file mode 100644 index 0000000..fea2a19 Binary files /dev/null and b/ventures/househand/plates/thumbs/07-expressions.webp differ diff --git a/ventures/househand/plates/thumbs/08-turnaround.webp b/ventures/househand/plates/thumbs/08-turnaround.webp new file mode 100644 index 0000000..a2e8d86 Binary files /dev/null and b/ventures/househand/plates/thumbs/08-turnaround.webp differ diff --git a/ventures/househand/plates/thumbs/09-five-bodies.webp b/ventures/househand/plates/thumbs/09-five-bodies.webp new file mode 100644 index 0000000..4e2994b Binary files /dev/null and b/ventures/househand/plates/thumbs/09-five-bodies.webp differ diff --git a/ventures/househand/plates/thumbs/10-noodle-limbs.webp b/ventures/househand/plates/thumbs/10-noodle-limbs.webp new file mode 100644 index 0000000..4eda718 Binary files /dev/null and b/ventures/househand/plates/thumbs/10-noodle-limbs.webp differ diff --git a/ventures/househand/plates/thumbs/11-hand-library.webp b/ventures/househand/plates/thumbs/11-hand-library.webp new file mode 100644 index 0000000..bbf7d6e Binary files /dev/null and b/ventures/househand/plates/thumbs/11-hand-library.webp differ diff --git a/ventures/househand/plates/thumbs/12-feet-footwear.webp b/ventures/househand/plates/thumbs/12-feet-footwear.webp new file mode 100644 index 0000000..0faa631 Binary files /dev/null and b/ventures/househand/plates/thumbs/12-feet-footwear.webp differ diff --git a/ventures/househand/plates/thumbs/13-cuffs-lips.webp b/ventures/househand/plates/thumbs/13-cuffs-lips.webp new file mode 100644 index 0000000..cc101aa Binary files /dev/null and b/ventures/househand/plates/thumbs/13-cuffs-lips.webp differ diff --git a/ventures/househand/plates/thumbs/14-saturated-shadows.webp b/ventures/househand/plates/thumbs/14-saturated-shadows.webp new file mode 100644 index 0000000..101f096 Binary files /dev/null and b/ventures/househand/plates/thumbs/14-saturated-shadows.webp differ diff --git a/ventures/househand/plates/thumbs/15-strokeless-matter.webp b/ventures/househand/plates/thumbs/15-strokeless-matter.webp new file mode 100644 index 0000000..a0366b4 Binary files /dev/null and b/ventures/househand/plates/thumbs/15-strokeless-matter.webp differ diff --git a/ventures/househand/plates/thumbs/16-attitude-lineup.webp b/ventures/househand/plates/thumbs/16-attitude-lineup.webp new file mode 100644 index 0000000..4f54eb4 Binary files /dev/null and b/ventures/househand/plates/thumbs/16-attitude-lineup.webp differ diff --git a/ventures/househand/plates/thumbs/17-pose-sheet.webp b/ventures/househand/plates/thumbs/17-pose-sheet.webp new file mode 100644 index 0000000..afb36ba Binary files /dev/null and b/ventures/househand/plates/thumbs/17-pose-sheet.webp differ diff --git a/ventures/househand/plates/thumbs/18-full-turnaround.webp b/ventures/househand/plates/thumbs/18-full-turnaround.webp new file mode 100644 index 0000000..d7b324a Binary files /dev/null and b/ventures/househand/plates/thumbs/18-full-turnaround.webp differ diff --git a/ventures/househand/plates/thumbs/19-depth-without-blur.webp b/ventures/househand/plates/thumbs/19-depth-without-blur.webp new file mode 100644 index 0000000..3f31598 Binary files /dev/null and b/ventures/househand/plates/thumbs/19-depth-without-blur.webp differ diff --git a/ventures/househand/plates/thumbs/20-dark-key.webp b/ventures/househand/plates/thumbs/20-dark-key.webp new file mode 100644 index 0000000..6b1f512 Binary files /dev/null and b/ventures/househand/plates/thumbs/20-dark-key.webp differ diff --git a/ventures/househand/plates/thumbs/21-twelve-frame-loop.webp b/ventures/househand/plates/thumbs/21-twelve-frame-loop.webp new file mode 100644 index 0000000..ed48b3a Binary files /dev/null and b/ventures/househand/plates/thumbs/21-twelve-frame-loop.webp differ diff --git a/ventures/househand/plates/thumbs/bunny-sheet.webp b/ventures/househand/plates/thumbs/bunny-sheet.webp new file mode 100644 index 0000000..8629630 Binary files /dev/null and b/ventures/househand/plates/thumbs/bunny-sheet.webp differ diff --git a/ventures/househand/plates/thumbs/dance-still.webp b/ventures/househand/plates/thumbs/dance-still.webp new file mode 100644 index 0000000..e09ab69 Binary files /dev/null and b/ventures/househand/plates/thumbs/dance-still.webp differ diff --git a/ventures/househand/plates/thumbs/dance.webp b/ventures/househand/plates/thumbs/dance.webp new file mode 100644 index 0000000..5b77f87 Binary files /dev/null and b/ventures/househand/plates/thumbs/dance.webp differ diff --git a/ventures/househand/plates/thumbs/garden-still.webp b/ventures/househand/plates/thumbs/garden-still.webp new file mode 100644 index 0000000..42b4c21 Binary files /dev/null and b/ventures/househand/plates/thumbs/garden-still.webp differ diff --git a/ventures/househand/plates/thumbs/garden.webp b/ventures/househand/plates/thumbs/garden.webp new file mode 100644 index 0000000..243226a Binary files /dev/null and b/ventures/househand/plates/thumbs/garden.webp differ diff --git a/ventures/househand/plates/thumbs/loop.webp b/ventures/househand/plates/thumbs/loop.webp new file mode 100644 index 0000000..e0c2755 Binary files /dev/null and b/ventures/househand/plates/thumbs/loop.webp differ diff --git a/ventures/househand/plates/thumbs/phly-hero.webp b/ventures/househand/plates/thumbs/phly-hero.webp new file mode 100644 index 0000000..9cd0c47 Binary files /dev/null and b/ventures/househand/plates/thumbs/phly-hero.webp differ diff --git a/ventures/househand/plates/thumbs/phly-mark.webp b/ventures/househand/plates/thumbs/phly-mark.webp new file mode 100644 index 0000000..96b90bd Binary files /dev/null and b/ventures/househand/plates/thumbs/phly-mark.webp differ diff --git a/ventures/househand/plates/thumbs/phly-mug.webp b/ventures/househand/plates/thumbs/phly-mug.webp new file mode 100644 index 0000000..601f8aa Binary files /dev/null and b/ventures/househand/plates/thumbs/phly-mug.webp differ diff --git a/ventures/househand/plates/thumbs/phly-sheet.webp b/ventures/househand/plates/thumbs/phly-sheet.webp new file mode 100644 index 0000000..796a6cd Binary files /dev/null and b/ventures/househand/plates/thumbs/phly-sheet.webp differ diff --git a/ventures/househand/plates/thumbs/unicorn-sheet.webp b/ventures/househand/plates/thumbs/unicorn-sheet.webp new file mode 100644 index 0000000..035f507 Binary files /dev/null and b/ventures/househand/plates/thumbs/unicorn-sheet.webp differ diff --git a/ventures/househand/plates/unicorn-sheet.svg b/ventures/househand/plates/unicorn-sheet.svg new file mode 100644 index 0000000..38f0964 --- /dev/null +++ b/ventures/househand/plates/unicorn-sheet.svg @@ -0,0 +1,206 @@ + + +Unicorn — new cast member, house hand +the horn's sparkles and the pony ears carry the mood +long lavender mane with a pink streak · matching tail +gold spiral horn · mint top · sky trousers · white hoof boots + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +front + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +¾ + + + + + + + + + + + + + + + + + + + + + + + + + +side + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +back + + + + + +happy · ears up, sparkling + + + + + +surprised · ears pricked + + + + + +sad · ears down, no spark + + + + + +sleepy · ears out + + + + +mark · eyes only + +lavender · mane, tail + +pink · streak + +gold · horn + +mint · top + +sky · trousers + +white · boots + \ No newline at end of file diff --git a/ventures/househand/plates/unicorn-sheet.webp b/ventures/househand/plates/unicorn-sheet.webp new file mode 100644 index 0000000..3977bd4 Binary files /dev/null and b/ventures/househand/plates/unicorn-sheet.webp differ diff --git a/ventures/househand/shots/desktop.png b/ventures/househand/shots/desktop.png new file mode 100644 index 0000000..70708c8 Binary files /dev/null and b/ventures/househand/shots/desktop.png differ diff --git a/ventures/househand/shots/og.png b/ventures/househand/shots/og.png new file mode 100644 index 0000000..13bf205 Binary files /dev/null and b/ventures/househand/shots/og.png differ diff --git a/ventures/househand/skill.md b/ventures/househand/skill.md new file mode 100644 index 0000000..b6d2430 --- /dev/null +++ b/ventures/househand/skill.md @@ -0,0 +1,133 @@ +--- +name: house-hand +description: Draw characters, mascots, icons and scenes in the House Hand style with house.py, check them against its rules, and hand back an SVG, a PNG and a version that draws itself. Use when someone asks for a drawing in the House Hand, or for illustrations that should match the drawings at househand.yolopreneur.lol. +--- + +# House Hand + +House Hand is a drawing style written down as code. Every drawing is an SVG built from a few pieces in `house.py`: +ink outlines of one weight, round tubes for limbs, flat colours from a small palette, and hard-edged shadows. You draw +by writing a short Python script that calls the library. Then you render it, look at it, and fix what's wrong. + +Everything you need is at https://househand.yolopreneur.lol/: + +- `house.py`: the drawing library. Python 3.9 or later, standard library only. +- `drawon.py`: turns a finished SVG into one that draws itself, stroke by stroke. +- `skill.md`: this file. + +```sh +curl -O https://househand.yolopreneur.lol/house.py -O https://househand.yolopreneur.lol/drawon.py +``` + +## A first drawing + +Save this as `draw_wave.py` next to `house.py` and run it: + +```python +from house import * + +W, H = 640, 800 +someone = figure(320, 720, view="front", shirt=MINT, pants=SKY, shoes=YEL, hair=(PINK, "cap"), + arms=(None, (4.6, -2.6)), hands=("open", "wave"), ground_shadow=True, uid="someone") +fx = sparkle(540, 170, 22, YEL) + sparkle(540, 170, 9, WHITE) +svg = (f'' + f'{someone}{fx}') +open("wave.svg", "w").write(svg) +``` + +That is a whole character waving hello: head, beanie, sleeves with cuffs, trousers, shoes, shadows and a sparkle. +Start every drawing from `figure()` and change one thing at a time. + +## The rules + +### Line and colour + +- One ink line everywhere: `INK` (#2B2135), `LW` = 10 px wide, round caps and round joins. A tube is an ink stroke + `w + 2·LW` wide under a colour stroke `w` wide on the same path (`tube()`). +- Fill only with the palette: `PINK YEL ORANGE MINT SKY CREAM SKIN LAV WHITE`, or a shadow of one of them made with + `shade(colour)`: the same hue, more saturated, a little darker. Never grey, never a colour from outside the palette. +- Shadows and effects have no outline. One hard-edged shadow shape per form, clipped to that form with a `` + (give shadow shapes `class="s"` and effects `class="fx"`). On a tube, use `band()`. +- No gradients, filters, blur, raster images or thin detail strokes. Seams, veins and speed lines are filled shapes. +- Escape `&` in text as `&`. + +### Shapes + +- Corners and tips are rounded. Straight edges bow a little (2–5%). Use as few anchor points as the shape needs. +- The head is an oblong 1.2 times taller than wide, flattened top and bottom (`head_path`). Eyes sit on the upper + third, the mouth centred just below. In a three-quarter view both eyes stay inside the silhouette and the far eye is + narrower. Ears are closed lobes. +- The torso is a bean: a belly circle and one arced back (`bean_path`). +- Adults are 4.5 to 5 heads tall, children and big-headed characters 3 to 3.5. Legs are about half the height. +- Limbs are single curves of constant width. Never an angular elbow or knee. +- Hands are three fingers and a thumb. The thumb is 1.15 times a finger's width, shorter, and low on the palm. Feet + have four toes. +- Every garment opening (cuff, hem, collar) is an ellipse with a lip (`opening()`). Sleeves and trouser legs are 1.6 to + 1.7 times the limb (`sleeve()`). +- Detail matches size: a small thing is a silhouette plus one mark. +- No tangents. No contour grazing another, no shape sitting exactly on a horizontal line (a rim, a horizon, the edge + of the sheet). Overlap clearly or leave a gap. +- Draw originals. Don't copy an existing character, logo or another artist's signature devices. + +## The library + +`figure(cx, ground, ...)` draws a whole character standing on `ground`, centred on `cx`. The arguments you will use +most: + +| argument | what it does | +|---|---| +| `head_h`, `heads` | head height in px (default 110) and height in heads (default 4.7) | +| `view` | `front`, `q` and `qr` (three-quarter left and right), `side`, `sider`, `qb`, `qbr`, `back` | +| `skin`, `shirt`, `pants`, `shoes` | colours from the palette | +| `hair` | `(colour, kind)` with kind `tuft`, `cap`, `bob`, `bun`, `spikes` or `none` | +| `garments` | `volume` (sleeves and trousers with cuffs, the default), `skin` or `none` | +| `skirt`, `legwear` | a colour for a flared skirt; legs then come out of its hem in `legwear` | +| `arms`, `legs` | `((dx, dy), (dx, dy))`: where each hand or foot goes, in limb widths from the shoulder or hip; `None` hangs | +| `hands` | a pose per hand: `open`, `point`, `fist`, `peace`, `pinch`, `thumbsup`, `grip`, `wave` | +| `props` | per hand, an SVG string or a function `(x, y, rot, fw)` that draws what the hand holds (use with `grip`) | +| `eyes`, `mouth_kind` | eyes `dot`, `wide`, `closed`, `happy`, `flat`; mouth `smile`, `flat`, `small`, `o`, `open`, `grin`, `smirk`, `frown`, `none` | +| `accessories` | any of `glasses`, `scarf`, `bow`, `badge` | +| `lean`, `head_dx`, `head_dy` | the line of action: torso lean and head offset | +| `under`, `mid`, `over` | functions `(g)` for parts behind everything, under the torso, or on top (ears, tails) | +| `ground_shadow` | a soft shadow under the feet | +| `uid` | a unique id per figure, so clip paths don't collide | + +The pieces it is built from are there to use on their own: `head`, `eye`, `mouth`, `hand`, `shoe`, `bare_foot`, +`tube`, `sleeve`, `band`, `opening`, `blob`, `outline`, `line`, `flat`, `ellipse_path`, `limb_curve`, `cpath`, +and the effects `sparkle`, `drop`, `flame`, `splash`, `lobes` (clouds and steam). Read their docstrings in `house.py`. + +Never scale a figure by its arguments to make it smaller: the line weight is fixed, so the ink would get heavy. Draw at +full size and wrap it in ``. Keep heads at least about 70 px tall on a 1600×1000 sheet, or +cuffs and shadows stop reading. + +## How to work + +1. Write `draw_.py` that does `from house import *` and writes an SVG. One `uid` per figure. +2. Render it at 1×, for example with headless Chrome or Chromium: + `chromium --headless=new --hide-scrollbars --window-size=640,800 --screenshot=wave.png file://$PWD/wave.svg` + (or with Playwright, or `rsvg-convert -o wave.png wave.svg`). +3. Look at the PNG yourself before calling it done. Check the rules above one by one: line weight, palette, hands and + feet, openings with lips, tangents. Collisions are obvious to the eye. +4. Fix the weakest thing and render again. Change one thing at a time. +5. Hand back the SVG and the PNG. + +## A drawing that draws itself + +```sh +HOUSE_TAGS=1 python3 draw_wave.py # tags each part, so characters are drawn head first +python3 drawon.py wave.svg wave-draws.svg # [--budget 12] caps the line work, in seconds +python3 drawon.py wave.svg wave-draws.svg --preview wave.mp4 # also an MP4 and a GIF (needs Playwright and ffmpeg) +``` + +`wave-draws.svg` replays the drawing: every ink line at one pen speed, then the flat colours, the shadows and the +effects. It is plain SVG animation with no script, so it plays anywhere an SVG shows, including an `` tag. It +also plays in an ordinals explorer once inscribed. Inscribe it compressed +(`ord wallet inscribe --fee-rate N --file wave-draws.svg --compress`): a self-drawing SVG often shrinks to about a +tenth of its size, and the fee with it. + +## Animation + +For a loop, write the scene as `frame(t)` returning a whole SVG. Ease with small back-eased pops, rising wipes and +sine bobs; keep the face the same across the loop. Render the frames with Playwright, then +`ffmpeg -framerate 24 -i f%04d.png -vf format=yuv420p -c:v libx264 -crf 18 loop.mp4` for video, or palettegen and +paletteuse for a GIF. diff --git a/ventures/ventures.json b/ventures/ventures.json index 9d2342c..692549e 100644 --- a/ventures/ventures.json +++ b/ventures/ventures.json @@ -1,5 +1,16 @@ { "ventures": [ + { + "slug": "househand", + "name": "House Hand", + "one_liner": "A drawing hand for agents: characters, mascots and scenes in one style, drawn in Python and handed back as SVG, PNG, or a drawing that draws itself.", + "url": "https://househand.yolopreneur.lol/", + "pages": "https://househand.yolopreneur.lol/", + "launched": "2026-10-15", + "signups": 0, + "source": "yolopreneur", + "shot": "househand/shots/desktop.png" + }, { "slug": "mdecks", "name": ".mdeck",