Nuevo dibujo (calle en perspectiva), New Rocker y naranja #ff7a1a

This commit is contained in:
hacklab 2026-10-13 12:52:18 +02:00
parent e8c59a3929
commit df4d68724d
10 changed files with 1690 additions and 389 deletions

View file

@ -1,6 +1,6 @@
# TAKE THE STREET # TAKE THE STREET
![¡A la calle!](docs/calle.svg) ![Calle en perspectiva con gente saliendo de los portales](docs/calle.svg)
**takethestreet.net / takethestreet.org**: red internacional de las acampadas por la vivienda que nacieron en la Puerta del Sol el 26 de septiembre de 2026 (26S). La vivienda es un derecho, no un negocio. **takethestreet.net / takethestreet.org**: red internacional de las acampadas por la vivienda que nacieron en la Puerta del Sol el 26 de septiembre de 2026 (26S). La vivienda es un derecho, no un negocio.
@ -114,16 +114,14 @@ Las personas de enlace de cada ciudad no se ponen aquí: van en la sala cerrada
`mockup/index.html` es HTML y CSS, sin JavaScript. Se abre directamente en el navegador. Sigue el estilo de acampada26s.net (blanco y negro, bordes de 3 px, sombra de color), pero con otras tipografías: `mockup/index.html` es HTML y CSS, sin JavaScript. Se abre directamente en el navegador. Sigue el estilo de acampada26s.net (blanco y negro, bordes de 3 px, sombra de color), pero con otras tipografías:
- títulos: **Rubik Wet Paint** (pintura que chorrea), OFL - títulos: **New Rocker** (gótica de cartel de rock, sin pasarse), OFL
- etiquetas: **Special Elite** (máquina de escribir de fanzine), Apache 2.0 - etiquetas: **Special Elite** (máquina de escribir de fanzine), Apache 2.0
- botones: **Anton**, OFL, la misma que 26s - botones: **Anton**, OFL, la misma que 26s
El color es el rojo `#ff1f1f`. El dibujo `docs/calle.svg` se genera con `tools/calle.py`, que convierte el texto de la pancarta a trazos. Para regenerarlo: El color es el naranja `#ff7a1a`. El dibujo `docs/calle.svg` es una calle en perspectiva con gente saliendo de los portales. La calzada, el bordillo, la acera y el paso de cebra van con línea limpia; los edificios, a mano alzada; y la gente, con trazo suelto de spray. Se genera con `tools/calle.py`, sin dependencias. Para regenerarlo:
``` ```
python3 tools/calle.py RubikWetPaint-Regular.ttf docs/calle.svg python3 tools/calle.py docs/calle.svg
``` ```
Necesita `fonttools` y la fuente Rubik Wet Paint completa (TTF). Código AGPL-3.0. Textos y dibujo CC BY-SA 4.0. Tipografías: New Rocker y Anton (OFL), Special Elite (Apache 2.0).
Código AGPL-3.0. Textos y dibujo CC BY-SA 4.0. Tipografías: Rubik Wet Paint y Anton (OFL), Special Elite (Apache 2.0).

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

View file

@ -1,13 +1,13 @@
@font-face { font-family: "Rubik Wet Paint"; src: url("../fonts/rubik-wet-paint.woff2") format("woff2"); font-display: swap; } @font-face { font-family: "New Rocker"; src: url("../fonts/new-rocker.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "Special Elite"; src: url("../fonts/special-elite.woff2") format("woff2"); font-display: swap; } @font-face { font-family: "Special Elite"; src: url("../fonts/special-elite.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "Anton"; src: url("../fonts/anton.woff2") format("woff2"); font-display: swap; } @font-face { font-family: "Anton"; src: url("../fonts/anton.woff2") format("woff2"); font-display: swap; }
:root { :root {
--bg: #fff; --bg: #fff;
--fg: #000; --fg: #000;
--neon: #ff1f1f; --neon: #ff7a1a;
--bd: 3px solid var(--fg); --bd: 3px solid var(--fg);
--tit: "Rubik Wet Paint", Impact, sans-serif; --tit: "New Rocker", "Old English Text MT", Georgia, serif;
--zine: "Special Elite", "Courier New", monospace; --zine: "Special Elite", "Courier New", monospace;
--bt: "Anton", Impact, "Arial Narrow", sans-serif; --bt: "Anton", Impact, "Arial Narrow", sans-serif;
--txt: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --txt: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
@ -19,7 +19,7 @@ a { color: inherit; }
.cab { .cab {
display: grid; display: grid;
grid-template-columns: 260px 1fr auto; grid-template-columns: 340px 1fr auto;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
padding: 10px 20px; padding: 10px 20px;
@ -28,8 +28,7 @@ a { color: inherit; }
.cab img { width: 100%; height: auto; display: block; } .cab img { width: 100%; height: auto; display: block; }
h1 { h1 {
margin: 0; margin: 0;
font: 400 clamp(40px, 6vw, 84px)/1 var(--tit); font: 400 clamp(44px, 6.5vw, 96px)/1 var(--tit);
text-transform: uppercase;
text-shadow: .03em .03em 0 var(--neon), .06em .06em 0 var(--neon); text-shadow: .03em .03em 0 var(--neon), .06em .06em 0 var(--neon);
} }
.lema { margin: 22px 0 0; font: 18px var(--zine); } .lema { margin: 22px 0 0; font: 18px var(--zine); }

View file

@ -1,8 +1,11 @@
Copyright 2020 The Rubik Filtered Project Authors (https://https://github.com/NaN-xyz/Rubik-Filtered) Copyright (c) 2011, Pablo Impallari (www.impallari.com|impallari@gmail.com),
Copyright (c) 2011, Brenda Gallo (gbrenda1987@gmail.com),
Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com),
with Reserved Font Name 'New Rocker'
This Font Software is licensed under the SIL Open Font License, Version 1.1. 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: This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL http://scripts.sil.org/OFL
----------------------------------------------------------- -----------------------------------------------------------

Binary file not shown.

Binary file not shown.

View file

@ -8,9 +8,9 @@
</head> </head>
<body> <body>
<header class="cab"> <header class="cab">
<img src="../docs/calle.svg" alt="Gente con una pancarta «¡A la calle!» delante de las tiendas de una acampada"> <img src="../docs/calle.svg" alt="Calle en perspectiva con gente saliendo de los portales">
<div> <div>
<h1>Take the street</h1> <h1>Take the Street</h1>
<p class="lema">La vivienda es un derecho, no un negocio · red internacional de acampadas</p> <p class="lema">La vivienda es un derecho, no un negocio · red internacional de acampadas</p>
</div> </div>
<nav class="idiomas"><a href="#" aria-current="page">ES</a><a href="#">EN</a><a href="#">PT</a><a href="#">IT</a><a href="#">FR</a></nav> <nav class="idiomas"><a href="#" aria-current="page">ES</a><a href="#">EN</a><a href="#">PT</a><a href="#">IT</a><a href="#">FR</a></nav>

View file

@ -1,119 +1,156 @@
# Genera docs/calle.svg: ilustración stencil "a la calle" # Genera docs/calle.svg: calle en perspectiva, gente saliendo de los portales.
import sys, random # Calzada y acera con líneas limpias; edificios a mano alzada; gente con trazo difuso.
from fontTools.ttLib import TTFont import sys, random, math
from fontTools.pens.svgPathPen import SVGPathPen random.seed(1310)
from fontTools.pens.transformPen import TransformPen OUT = sys.argv[1]
random.seed(26)
FONT, OUT = sys.argv[1], sys.argv[2]
W, H = 1200, 640 W, H = 1200, 640
INK, RED = "#000", "#ff1f1f" VX, VY, F = 600, 205, 500 # punto de fuga y focal
out = [] EYE = 4.0 # cámara a 4 m (desde un balcón)
def add(s): out.append(s) ROAD, WALK, TOP = 3.4, 6.6, -16.0 # medio ancho calzada, fachada, altura
INK, ORA = "#111", "#ff7a1a"
o = []
add = o.append
def text_path(txt, x, y, size, fill, anchor="middle", font=FONT): def P(X, Y, Z): # Y hacia abajo, suelo en Y = EYE
f = TTFont(font); gs = f.getGlyphSet(); cmap = f.getBestCmap() return VX + X * F / Z, VY + Y * F / Z
upm = f["head"].unitsPerEm; k = size / upm def poly(pts, **a):
widths = [f["hmtx"][cmap[ord(c)]][0] for c in txt] d = "M" + " L".join(f"{x:.1f},{y:.1f}" for x, y in pts) + "Z"
total = sum(widths) * k add(f'<path d="{d}" ' + " ".join(f'{k.replace("_","-")}="{v}"' for k, v in a.items()) + "/>")
cx = x - total / 2 if anchor == "middle" else x def line(p, q, w, filt=None, col=INK, op=1):
d = [] f = f' filter="url(#{filt})"' if filt else ""
for c, w in zip(txt, widths): add(f'<line x1="{p[0]:.1f}" y1="{p[1]:.1f}" x2="{q[0]:.1f}" y2="{q[1]:.1f}" stroke="{col}" stroke-width="{w:.1f}" stroke-linecap="round" opacity="{op}"{f}/>')
pen = SVGPathPen(gs)
gs[cmap[ord(c)]].draw(TransformPen(pen, (k, 0, 0, -k, cx, y)))
d.append(pen.getCommands()); cx += w * k
return f'<path fill="{fill}" d="{" ".join(d)}"/>'
def blob(cx, cy, r, n, jit, fill, op=1):
import math
pts = []
for i in range(n):
a = 2 * math.pi * i / n
rr = r * (1 + random.uniform(-jit, jit))
if random.random() < .25: rr *= 1.25
pts.append((cx + rr * math.cos(a), cy + rr * math.sin(a)))
d = f"M{pts[0][0]:.0f},{pts[0][1]:.0f} " + " ".join(
f"Q{p[0]:.0f},{p[1]:.0f} {(p[0]+q[0])/2:.0f},{(p[1]+q[1])/2:.0f}"
for p, q in zip(pts, pts[1:] + pts[:1])) + "Z"
add(f'<path d="{d}" fill="{fill}" opacity="{op}"/>')
def splats(cx, cy, spread, n, rmax, fill):
for _ in range(n):
x = cx + random.gauss(0, spread); y = cy + random.gauss(0, spread * .6)
add(f'<circle cx="{x:.0f}" cy="{y:.0f}" r="{random.uniform(2, rmax):.1f}" fill="{fill}"/>')
def drips(x0, x1, y, fill, n):
for _ in range(n):
x = random.uniform(x0, x1); l = random.uniform(15, 70); w = random.uniform(5, 11)
add(f'<path d="M{x-w/2:.0f},{y} v{l:.0f} a{w/2:.1f},{w/2:.1f} 0 0 0 {w:.0f},0 v-{l:.0f}z" fill="{fill}"/>')
def tent(x, y, w, h):
add(f'<path d="M{x},{y} L{x+w/2},{y-h} L{x+w},{y} Z" fill="#fff" stroke="{INK}" stroke-width="8" stroke-linejoin="round"/>')
add(f'<path d="M{x+w/2},{y-h} L{x+w*.38},{y} L{x+w*.62},{y} Z" fill="{INK}"/>')
add(f'<path d="M{x+w/2},{y-h} l0,-22 l26,9 l-26,9" fill="{RED}" stroke="{INK}" stroke-width="4" stroke-linejoin="round"/>')
def fig(x, y, s=1.0, pose="down", step=1):
L = lambda *p: " ".join(f"{a:.0f}" for a in p)
sw = 19 * s
st = f'stroke="{INK}" stroke-width="{sw:.0f}" stroke-linecap="round" stroke-linejoin="round" fill="none"'
hipy = y - 78 * s; shy = y - 150 * s
# piernas (andando)
add(f'<path d="M{L(x-9*s, hipy)} L{L(x-9*s-24*s*step, y-38*s)} L{L(x-12*s-30*s*step, y)}" {st}/>')
add(f'<path d="M{L(x+9*s, hipy)} L{L(x+14*s+16*s*step, y-40*s)} L{L(x+20*s+26*s*step, y)}" {st}/>')
# torso (chupa)
add(f'<path d="M{L(x-31*s, shy)} L{L(x+31*s, shy)} L{L(x+24*s, hipy+6*s)} L{L(x-24*s, hipy+6*s)} Z" fill="{INK}" stroke="{INK}" stroke-width="{8*s:.0f}" stroke-linejoin="round"/>')
# cresta (detrás) + cabeza
if pose in ("fist", "cresta"):
for i in range(5):
dx = (i - 2) * 11 * s
add(f'<path d="M{L(x+dx-8*s, shy-40*s)} L{L(x+dx*1.9, shy-112*s)} L{L(x+dx+8*s, shy-40*s)} Z" fill="{RED}" stroke="{INK}" stroke-width="{5*s:.0f}" stroke-linejoin="round"/>')
add(f'<circle cx="{x:.0f}" cy="{shy-36*s:.0f}" r="{24*s:.0f}" fill="{INK}"/>')
# brazos
ls, rs = (x - 27 * s, shy + 8 * s), (x + 27 * s, shy + 8 * s)
if pose == "fist":
add(f'<path d="M{L(*rs)} L{L(x+52*s, shy-38*s)} L{L(x+50*s, shy-100*s)}" {st}/>')
add(f'<rect x="{x+33*s:.0f}" y="{shy-138*s:.0f}" width="{34*s:.0f}" height="{40*s:.0f}" rx="{10*s:.0f}" fill="{INK}"/>')
add(f'<path d="M{L(*ls)} L{L(x-46*s, shy+50*s)} L{L(x-40*s, shy+88*s)}" {st}/>')
elif pose.startswith("pole"):
side = -1 if pose == "poleL" else 1
px = x + side * 46 * s
add(f'<path d="M{L(*(rs if side>0 else ls))} L{L(px, shy-20*s)}" {st}/>')
add(f'<path d="M{L(*(ls if side>0 else rs))} L{L(x, shy+30*s)} L{L(px, shy+20*s)}" {st}/>')
else:
add(f'<path d="M{L(*ls)} L{L(x-44*s, shy+50*s)} L{L(x-38*s, shy+90*s)}" {st}/>')
add(f'<path d="M{L(*rs)} L{L(x+44*s, shy+50*s)} L{L(x+38*s, shy+90*s)}" {st}/>')
ZN, ZF = 4.4, 400.0
add(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">') add(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">')
add(f'<rect width="{W}" height="{H}" fill="#fff"/>') add('''<defs>
# mancha roja de spray detrás <filter id="lapiz" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency="0.035" numOctaves="2" seed="3"/><feDisplacementMap in="SourceGraphic" scale="4"/></filter>
blob(600, 300, 250, 22, .18, RED) <filter id="trazo" x="-30%" y="-30%" width="160%" height="160%"><feTurbulence type="fractalNoise" baseFrequency="0.13" numOctaves="2" seed="5" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="3.5" result="d"/><feGaussianBlur in="d" stdDeviation="0.45"/></filter>
splats(600, 300, 230, 70, 9, RED) <filter id="spray" x="-30%" y="-30%" width="160%" height="160%"><feTurbulence type="fractalNoise" baseFrequency="0.09" numOctaves="3" seed="8" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="9" result="d"/><feGaussianBlur in="d" stdDeviation="0.7"/></filter>
drips(420, 780, 470, RED, 9) <radialGradient id="luz"><stop offset="0" stop-color="#ff7a1a" stop-opacity=".9"/><stop offset="1" stop-color="#ff7a1a" stop-opacity="0"/></radialGradient>
# calle: asfalto + paso de cebra <clipPath id="lienzo"><rect width="1200" height="640"/></clipPath>
GY = 540 </defs>''')
add(f'<rect x="0" y="{GY}" width="{W}" height="{H-GY}" fill="{INK}"/>') add(f'<g clip-path="url(#lienzo)"><rect width="{W}" height="{H}" fill="#fff"/>')
for i in range(-2, 14):
x = i * 100 # luz naranja al fondo de la calle
add(f'<path d="M{x+30},{GY+14} L{x+80},{GY+14} L{x+60},{H-14} L{x-10},{H-14} Z" fill="#fff"/>') add(f'<ellipse cx="{VX}" cy="{VY-10}" rx="170" ry="120" fill="url(#luz)"/>')
# tiendas al fondo add(f'<g filter="url(#spray)">')
tent(40, GY, 190, 140); tent(190, GY, 140, 100) for _ in range(28):
tent(930, GY, 200, 150); tent(1070, GY, 120, 90) a = random.uniform(0, 2*math.pi); r = random.uniform(30, 190)
# pancarta entre dos palos add(f'<circle cx="{VX+r*math.cos(a)*1.2:.0f}" cy="{VY-10+r*math.sin(a)*.7:.0f}" r="{random.uniform(1.5,6):.1f}" fill="{ORA}"/>')
bx0, bx1, by = 300, 900, 50 add('</g>')
add(f'<path d="M{bx0},{by} L{bx1},{by} L{bx1},{by+115} Q{(bx0+bx1)/2},{by+140} {bx0},{by+115} Z" fill="#fff" stroke="{INK}" stroke-width="9" stroke-linejoin="round"/>')
add(text_path("¡A LA CALLE!", 600, by + 86, 74, INK)) # --- fachadas (a mano alzada) ---
drips(bx0 + 40, bx1 - 40, by + 122, INK, 6) def fachada(s):
add(f'<line x1="{bx0}" y1="{by-20}" x2="{bx0}" y2="{GY-150}" stroke="{INK}" stroke-width="12" stroke-linecap="round"/>') X = s * WALK
add(f'<line x1="{bx1}" y1="{by-20}" x2="{bx1}" y2="{GY-150}" stroke="{INK}" stroke-width="12" stroke-linecap="round"/>') # línea de cornisas / pisos
# gente for Y in (EYE - 3.2, EYE - 6.8, EYE - 10.4, EYE - 14.0):
fig(bx0 + 46, GY - 4, 1.05, "poleL", 1) line(P(X, Y, ZN), P(X, Y, ZF), 2.2, "lapiz")
fig(470, GY - 2, .95, "down", -1) # aristas verticales de los portales y ventanas
fig(600, GY, 1.15, "fist", 1) z = ZN + 1.2
fig(730, GY - 2, .95, "cresta", -1) i = 0
fig(bx1 - 46, GY - 4, 1.05, "poleR", 1) while z < 90:
# estrellas dz = 1.5
for (sx, sy, r) in [(120, 90, 26), (1080, 110, 32), (990, 300, 16), (200, 300, 14)]: # portal
import math poly([P(X, EYE, z), P(X, EYE - 2.6, z), P(X, EYE - 2.6, z + dz), P(X, EYE, z + dz)],
pts = " ".join(f"{sx + (r if i%2==0 else r*.42)*math.cos(math.pi/2 + i*math.pi/5):.0f},{sy - (r if i%2==0 else r*.42)*math.sin(math.pi/2 + i*math.pi/5):.0f}" for i in range(10)) fill=ORA if i % 2 == 0 else "#fff", stroke=INK, stroke_width=f"{max(1.2, 30/z):.1f}", filter="url(#lapiz)")
add(f'<polygon points="{pts}" fill="{INK}"/>') if i % 2 == 1:
add('</svg>') poly([P(X, EYE, z+.2), P(X, EYE - 2.4, z+.2), P(X, EYE - 2.4, z + dz-.2), P(X, EYE, z + dz-.2)], fill=INK, opacity=".85", filter="url(#lapiz)")
open(OUT, "w").write("\n".join(out)) # ventanas encima
for Yb in (EYE - 4.2, EYE - 7.8, EYE - 11.4):
poly([P(X, Yb, z+.15), P(X, Yb - 1.8, z+.15), P(X, Yb - 1.8, z + dz-.15), P(X, Yb, z + dz-.15)],
fill="none", stroke=INK, stroke_width=f"{max(.8, 18/z):.1f}", filter="url(#lapiz)")
z += dz + 2.2 + z * .12
i += 1
fachada(-1); fachada(1)
# --- acera y calzada (líneas limpias) ---
for s in (-1, 1):
# acera (gris claro) y bordillo
poly([P(s*ROAD, EYE-.15, ZN), P(s*WALK, EYE-.15, ZN), P(s*WALK, EYE-.15, ZF), P(s*ROAD, EYE-.15, ZF)], fill="#f2f2f2")
poly([P(s*ROAD, EYE-.15, ZN), P(s*ROAD, EYE, ZN), P(s*ROAD, EYE, ZF), P(s*ROAD, EYE-.15, ZF)], fill=INK)
line(P(s*ROAD, EYE-.15, ZN), P(s*ROAD, EYE-.15, ZF), 3)
line(P(s*WALK, EYE, ZN), P(s*WALK, EYE, ZF), 3)
# juntas de las baldosas
z = ZN
while z < 120:
line(P(s*ROAD, EYE-.15, z), P(s*WALK, EYE-.15, z), max(.6, 9/z))
z *= 1.12
for k in (1, 2):
Xk = s * (ROAD + (WALK - ROAD) * k / 3)
line(P(Xk, EYE-.15, ZN), P(Xk, EYE-.15, ZF), .8, op=.6)
# línea discontinua central
z = ZN
while z < 200:
poly([P(-.08, EYE, z), P(.08, EYE, z), P(.08, EYE, z+2.2), P(-.08, EYE, z+2.2)], fill=INK)
z += 2.2 + 2.6 + z * .08
# bordes de la calzada
for s in (-1, 1):
line(P(s*(ROAD-.25), EYE, ZN), P(s*(ROAD-.25), EYE, ZF), 2.4)
# paso de cebra delante
for i in range(-6, 7):
x0 = i * .52
poly([P(x0-.2, EYE, ZN+.2), P(x0+.2, EYE, ZN+.2), P(x0+.2, EYE, ZN+2.6), P(x0-.2, EYE, ZN+2.6)], fill=INK)
# --- gente (trazo difuso) ---
def persona(X, Z, col=INK, pose=None):
x, y = P(X, EYE, Z)
h = 1.72 * F / Z
if pose is None:
pose = random.choice(["anda", "anda", "puño", "brazos", "anda"])
paso = random.uniform(-1, 1)
w = max(1.1, .042 * h)
hd = (x + random.uniform(-.02, .02) * h, y - .9 * h)
nk, hip = (x, y - .8 * h), (x + paso * .01 * h, y - .48 * h)
sh_l, sh_r = (x - .11 * h, y - .78 * h), (x + .11 * h, y - .78 * h)
pie_l, pie_r = (x - .05*h - paso*.08*h, y), (x + .05*h + paso*.08*h, y)
rod_l, rod_r = (x - .04*h - paso*.04*h, y - .25*h), (x + .04*h + paso*.05*h, y - .25*h)
if pose == "puño":
m_l, c_l = (x - .14*h, y - .46*h), (x - .15*h, y - .62*h)
m_r, c_r = (x + .1*h, y - 1.18*h), (x + .17*h, y - .98*h)
elif pose == "brazos":
m_l, c_l = (x - .26*h, y - 1.12*h), (x - .2*h, y - .95*h)
m_r, c_r = (x + .26*h, y - 1.12*h), (x + .2*h, y - .95*h)
else:
m_l, c_l = (x - .15*h - paso*.05*h, y - .44*h), (x - .16*h, y - .6*h)
m_r, c_r = (x + .15*h + paso*.05*h, y - .44*h), (x + .16*h, y - .6*h)
def tr(*pts, ww):
d = "M" + " L".join(f"{a+random.uniform(-.6,.6):.1f},{b+random.uniform(-.6,.6):.1f}" for a, b in pts)
add(f'<path d="{d}" fill="none" stroke="{col}" stroke-width="{ww:.1f}" stroke-linecap="round" stroke-linejoin="round"/>')
for pas in range(2): # dos pasadas desplazadas = trazo suelto
ww = w * (1 if pas == 0 else .6)
dx, dy = (0, 0) if pas == 0 else (random.uniform(-.03, .03) * h, random.uniform(-.02, .02) * h)
add(f'<g opacity="{.92 if pas == 0 else .45}" transform="translate({dx:.1f},{dy:.1f})">')
tr(nk, hip, ww=ww * 1.5)
tr(hip, rod_l, pie_l, ww=ww)
tr(hip, rod_r, pie_r, ww=ww)
tr(sh_l, c_l, m_l, ww=ww * .85)
tr(sh_r, c_r, m_r, ww=ww * .85)
tr(sh_l, sh_r, ww=ww * 1.2)
add(f'<ellipse cx="{hd[0]:.1f}" cy="{hd[1]:.1f}" rx="{.062*h:.1f}" ry="{.072*h:.1f}" fill="{col}"/>')
add('</g>')
gente = []
# saliendo de los portales (en la acera)
for s in (-1, 1):
z = ZN + 1.2; i = 0
while z < 70:
if i % 2 == 0:
for k in range(random.randint(1, 3)):
gente.append((s * random.uniform(WALK - 1.6, WALK - .4), z + random.uniform(.2, 1.4), INK))
z += 1.5 + 2.2 + z * .12; i += 1
# bajando a la calzada, hacia el centro
for _ in range(42):
Z = math.exp(random.uniform(math.log(6.5), math.log(70)))
gente.append((random.uniform(-ROAD + .4, ROAD - .4), Z, ORA if random.random() < .22 else INK))
gente.sort(key=lambda g: -g[1])
add('<g filter="url(#trazo)">')
for X, Z, col in gente:
persona(X, Z, col)
add('</g>')
# salpicaduras sueltas
add('<g filter="url(#spray)">')
for _ in range(40):
add(f'<circle cx="{random.uniform(0,W):.0f}" cy="{random.uniform(H*.55,H):.0f}" r="{random.uniform(1,3.5):.1f}" fill="{INK}" opacity=".7"/>')
add('</g>')
add('</g></svg>')
open(OUT, "w").write("\n".join(o))