takethestreet/tools/calle.py
hacklab 94227e9838 Dibujo: calle en líneas negras con una piña de monigotes de colores
A partir del boceto 4: calle vista desde un primer piso (fachada, portales,
bordillos y línea del centro, todo en negro a pincel) y encima una piña de
monigotes de un trazo en zigzag, fuego en el centro y fríos alrededor.
Fuera los bocetos.
2026-10-13 13:44:05 +02:00

111 lines
6.5 KiB
Python

# Genera docs/calle.svg: una calle en perspectiva dibujada solo con líneas negras
# (bordillos, pie de fachada, portales, línea del centro) y, encima, una piña de
# monigotes de un solo trazo de spray en zigzag, de colores: fuego en el centro y
# fríos alrededor (a la manera de Suso33).
import sys, random, math
random.seed(4)
OUT = sys.argv[1]
W, H = 1600, 680
VX, VY, F, EYE = 1180, 110, 520, 5.0 # punto de fuga, focal, ojo en un primer piso (m)
FACH, KI, KD = -7.0, -4.5, 3.6 # fachada, bordillo izquierdo y derecho (m)
INK = "#111"
FUEGO = ["#fffbe6", "#ffe066", "#ffb627", "#ff7b1c", "#f04e23", "#c8102e"] # del centro hacia fuera
FRIO = ["#5b2a86", "#8e44c9", "#3b5bdb", "#6c7ae0", "#b07cd8", "#2a9d8f", "#111"]
o = []
add = o.append
def P(X, Yh, Z):
return VX + X * F / Z, VY + (EYE - Yh) * F / Z
def linea(a, b, w, op=.95):
add(f'<path d="M{a[0]:.1f},{a[1]:.1f} L{b[0]:.1f},{b[1]:.1f}" stroke="{INK}" stroke-width="{w:.1f}" stroke-linecap="round" opacity="{op}"/>')
Y0 = 100 # arriba solo hay blanco: se recorta
add(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 {Y0} {W} {H - Y0}" width="{W}" height="{H - Y0}">')
add('''<defs>
<filter id="spray" x="-30%" y="-10%" width="160%" height="120%"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" seed="3" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="2" result="d"/><feGaussianBlur in="d" stdDeviation=".6" result="b"/><feGaussianBlur in="SourceGraphic" stdDeviation="4" result="n"/><feComponentTransfer in="n" result="n2"><feFuncA type="linear" slope=".3"/></feComponentTransfer><feMerge><feMergeNode in="n2"/><feMergeNode in="b"/></feMerge></filter>
<filter id="brocha" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency="0.03 0.2" numOctaves="3" seed="4" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="5"/></filter>
<filter id="halo" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="45"/></filter>
<linearGradient id="gd" x1="0" y1="0" x2="1" y2="0"><stop offset=".6" stop-color="#fff"/><stop offset=".97" stop-color="#000"/></linearGradient>
<linearGradient id="gv" x1="0" y1="0" x2="0" y2="1"><stop offset=".05" stop-color="#000"/><stop offset=".3" stop-color="#fff"/></linearGradient>
<mask id="derecha" maskUnits="userSpaceOnUse" x="0" y="0" width="1600" height="680"><rect width="1600" height="680" fill="url(#gd)"/></mask>
<mask id="arriba" maskUnits="userSpaceOnUse" x="0" y="0" width="1600" height="680"><rect width="1600" height="680" fill="url(#gv)"/></mask>
</defs>''')
# --- la calle: solo líneas negras a pincel ---
ZN, ZF = 3.0, 90
add('<g mask="url(#derecha)"><g mask="url(#arriba)"><g filter="url(#brocha)" fill="none">')
linea(P(FACH, 0, ZN), P(FACH, 0, ZF), 7) # pie de la fachada
linea(P(KI, 0, ZN), P(KI, 0, ZF), 9) # bordillo
linea(P(KI + .18, 0, ZN), P(KI + .18, 0, ZF), 3, .7) # canto del bordillo
linea(P(KD, 0, 5), P(KD, 0, ZF), 6, .8) # bordillo de enfrente
z = 4.5
while z < 60: # línea discontinua del centro
linea(P(-.3, 0, z), P(-.3, 0, z + 2.2), 2 + 16 / z, .8)
z += 5
for zd in (4.0, 7.5, 11.5, 17.5, 26.0, 38.0): # portales: solo el contorno
a, b = P(FACH, 0, zd), P(FACH, 0, zd + 1.5)
c, d = P(FACH, 2.5, zd), P(FACH, 2.5, zd + 1.5)
w = 30 / zd + 2
linea(a, c, w); linea(c, d, w); linea(d, b, w)
linea(P(FACH, 2.8, zd - .3), P(FACH, 2.8, zd + 1.8), w * .6, .6) # dintel
add('</g></g></g>')
# --- monigotes de un trazo ---
def monigote(x, y, h, col, lean=0.0, paso=False, brazo=False):
sw = max(1.1, h * .011)
dy = sw * 2.0
top = y - h
pts, yy, i = [], top, 0
while yy <= y:
t = (yy - top) / h
cx = x + lean * h * (1 - t)
if t < .13: # cabeza
w = h * .05 * math.sin(math.pi * t / .13) ** .6 + sw * .3
elif t < .16: # cuello
w = h * .008
else: # cuerpo que se afila hacia los pies
u = (t - .16) / .84
w = h * (.062 * (1 - u) ** .8 + .014)
if paso and u > .5: # zancada: el zigzag se abre en dos
cx += (-1 if (i // 2) % 2 else 1) * h * .035 * (u - .5) / .5
w *= .6
pts.append((cx + (-1 if i % 2 else 1) * w + random.uniform(-.3, .3) * sw, yy))
yy += dy * random.uniform(.85, 1.15) * (.55 if t < .13 else 1)
i += 1
d = "M" + " L".join(f"{px:.1f},{py:.1f}" for px, py in pts)
add(f'<path d="{d}" fill="none" stroke="{col}" stroke-width="{sw:.1f}" stroke-linejoin="round" stroke-linecap="round" filter="url(#spray)"/>')
if brazo: # brazo en alto
bx, by = x + lean * h * .84 + h * .05, top + h * .2
p = [(bx + j / 9 * h * .06 + (-1 if j % 2 else 1) * h * .012, by - j / 9 * h * .30) for j in range(10)]
d = "M" + " L".join(f"{px:.1f},{py:.1f}" for px, py in p)
add(f'<path d="{d}" fill="none" stroke="{col}" stroke-width="{sw:.1f}" stroke-linecap="round" filter="url(#spray)"/>')
# reparto en metros sobre el suelo: la piña por la calzada y regueros desde los portales
CX, CZ, R = -.4, 8.5, 2.8 # centro de la piña (fuego) y radio
gente = []
def cabe(X, Z):
return all(math.hypot(X - a, (Z - b) * .8) > .5 for a, b in gente)
for _ in range(14000):
X, Z = random.gauss(-.2, 2.4), random.gauss(9.5, 4.2)
if KI - 1.5 < X < KD - .3 and 4.9 < Z < 32 and cabe(X, Z):
gente.append((X, Z))
for zd in (7.5, 11.5, 17.5): # salen de los portales hacia la calzada
for k in range(4):
X, Z = FACH + .6 + k * .7, zd + .75 - k * .5
if cabe(X, Z):
gente.append((X, Z))
add(f'<circle cx="{P(CX, 1, CZ)[0]:.0f}" cy="{P(CX, 1, CZ)[1]:.0f}" r="150" fill="#ffb627" opacity=".25" filter="url(#halo)"/>')
for X, Z in sorted(gente, key=lambda g: -g[1]): # de lejos a cerca
x, y = P(X, 0, Z)
h = 1.7 * F / Z * random.uniform(.9, 1.08)
dist = math.hypot(X - CX, Z - CZ) / R
if dist < 1:
col = FUEGO[min(len(FUEGO) - 1, int(max(0, dist + random.uniform(-.12, .12)) * len(FUEGO)))]
else:
col = random.choice(FRIO)
monigote(x, y, h, col, random.uniform(-.06, .04), random.random() < .35, random.random() < .1)
add('</svg>')
open(OUT, "w").write("\n".join(o))
print(f"{len(gente)} monigotes", file=sys.stderr)