takethestreet/tools/calle.py
hacklab 987893d33c Logotipo en Grindy Brush (negro), naranja más rojizo y dibujo nuevo
- logotipo: Grindy Brush todo en negro (o blanco en negativo), convertido a
  trazados; fuera las alternativas y las versiones con STREET en naranja.
  README: la fuente es demo de uso personal, hay que comprar la licencia
- naranja #ff7a1a -> #f04e23, solo en detalles
- docs/calle.svg: calle corta en diagonal, solo planta baja que se desvanece;
  de cada portal sale gente que avanza hacia la calzada (presencias con alguna
  zancada o puño en alto)
2026-10-13 13:28:18 +02:00

111 lines
7.5 KiB
Python

# Genera docs/calle.svg: una calle corta vista en diagonal. De cada portal sale
# gente que avanza hacia la calzada (presencias abstractas, a la manera de Suso33).
# Arriba no hay pisos: la fachada se desvanece, y la calle también hacia la derecha.
import sys, random, math
random.seed(26)
OUT = sys.argv[1]
W, H = 1600, 700
VX, VY, F, EYE = 1150, 210, 520, 1.6 # punto de fuga a la derecha, focal, ojo a 1,6 m
FX, KX = -5.0, -2.6 # fachada y bordillo (m)
INK, ORA = "#111", "#f04e23"
o = []
add = o.append
def P(X, Yh, Z): # Yh = altura sobre el suelo
return VX + X * F / Z, VY + (EYE - Yh) * F / Z
def pts(*ps):
return " ".join(f"{x:.1f},{y:.1f}" for x, y in ps)
def brush(a, b, w, n=4, col=INK, op=.9):
for _ in range(n):
j = lambda: random.uniform(-w, w) * .35
add(f'<line x1="{a[0]+j():.1f}" y1="{a[1]+j():.1f}" x2="{b[0]+j():.1f}" y2="{b[1]+j():.1f}" stroke="{col}" '
f'stroke-width="{random.uniform(w*.3, w*.65):.1f}" stroke-linecap="round" opacity="{random.uniform(op*.55, op):.2f}"/>')
HV = 570 # se recorta el suelo vacío de abajo
add(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {HV}" width="{W}" height="{HV}">')
add('''<defs>
<filter id="brocha" x="-10%" y="-10%" width="120%" height="120%"><feTurbulence type="fractalNoise" baseFrequency="0.02 0.12" numOctaves="3" seed="4" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="7"/></filter>
<filter id="pintura" x="-20%" y="-20%" width="140%" height="140%"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="2" seed="9" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="5" result="d"/><feGaussianBlur in="d" stdDeviation=".6"/></filter>
<filter id="sombra" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2"/></filter>
<linearGradient id="gv" x1="0" y1="0" x2="0" y2="1"><stop offset=".04" stop-color="#000"/><stop offset=".32" stop-color="#fff"/></linearGradient>
<linearGradient id="gh" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#000"/><stop offset=".07" stop-color="#fff"/><stop offset=".6" stop-color="#fff"/><stop offset=".93" stop-color="#000"/></linearGradient>
<linearGradient id="hueco" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#111"/><stop offset="1" stop-color="#111" stop-opacity=".75"/></linearGradient>
<mask id="arriba" maskUnits="userSpaceOnUse" x="0" y="0" width="1600" height="700"><rect width="1600" height="700" fill="url(#gv)"/></mask>
<mask id="derecha" maskUnits="userSpaceOnUse" x="0" y="0" width="1600" height="700"><rect width="1600" height="700" fill="url(#gh)"/></mask>
</defs>''')
ZN, ZF = 1.5, 70
# --- calle: suelo, bordillo y carretera; se va a blanco por la derecha ---
add('<g mask="url(#derecha)">')
add('<g filter="url(#brocha)">')
add(f'<polygon points="{pts(P(FX,0,ZN), P(FX,0,ZF), P(9,0,ZF), P(9,0,ZN))}" fill="#f2f0eb"/>')
add(f'<polygon points="{pts(P(FX,0,ZN), P(FX,0,ZF), P(KX,0,ZF), P(KX,0,ZN))}" fill="#e6e3dc"/>') # acera
brush(P(KX, 0, ZN), P(KX, 0, 40), 9, 5) # bordillo
brush(P(KX + .25, 0, ZN), P(KX + .25, 0, 34), 3, 2, col=ORA) # línea naranja de detalle
add('</g></g>')
# --- fachada solo a la altura de los portales: arriba se desvanece ---
add('<g mask="url(#arriba)"><g filter="url(#brocha)">')
add(f'<polygon points="{pts(P(FX,0,ZN), P(FX,3.4,ZN), P(FX,3.4,ZF), P(FX,0,ZF))}" fill="#f7f6f2"/>')
brush(P(FX, 0, ZN), P(FX, 0, ZF), 7, 4) # pie de la fachada
DOORS = [4.0, 7.5, 12.0, 18.5, 28.0]
for zd in DOORS:
d = [P(FX, 0, zd), P(FX, 2.5, zd), P(FX, 2.5, zd + 1.5), P(FX, 0, zd + 1.5)]
add(f'<polygon points="{pts(*d)}" fill="url(#hueco)"/>')
w = 90 / zd
brush(d[0], d[1], w, 3); brush(d[1], d[2], w, 3) # marco
if zd in (4.0, 12.0): # dintel en naranja
brush(P(FX, 2.7, zd - .1), P(FX, 2.7, zd + 1.6), w * .5, 2, col=ORA, op=.8)
add('</g></g>')
# --- presencias: salen de los portales hacia la calzada y hacia quien mira ---
def presencia(X, Z, paso, brazo):
fx, fy = P(X, 0, Z)
h = 1.75 * F / Z # alto en pantalla
lean = h * .09 # inclinados hacia delante (derecha)
sx, sy = fx + lean, fy - h * .80 # hombros
r = h * .095
w = h * .17
# sombra en el suelo, hacia atrás a la izquierda
add(f'<polygon points="{pts((fx-w*.3,fy),(fx-h*.75,fy-h*.16),(fx-h*.7,fy-h*.2),(fx+w*.3,fy-2))}" fill="{INK}" opacity=".18" filter="url(#sombra)"/>')
if paso: # zancada: tronco y dos piernas de pincel
hip = (fx + lean * .4, fy - h * .45)
add(f'<path d="M{sx-w/2:.1f},{sy:.1f} Q{sx-w*.45:.1f},{sy+h*.2:.1f} {hip[0]-w*.3:.1f},{hip[1]:.1f} '
f'L{hip[0]+w*.3:.1f},{hip[1]:.1f} Q{sx+w*.5:.1f},{sy+h*.2:.1f} {sx+w/2:.1f},{sy:.1f} Z" fill="{INK}"/>')
for fxx, fyy in ((fx - h * .16, fy), (fx + h * .17, fy - h * .03)):
add(f'<path d="M{hip[0]-w*.28:.1f},{hip[1]-2:.1f} Q{(hip[0]+fxx)/2-w*.12:.1f},{(hip[1]+fyy)/2:.1f} {fxx-w*.07:.1f},{fyy:.1f} '
f'L{fxx+w*.09:.1f},{fyy:.1f} Q{(hip[0]+fxx)/2+w*.14:.1f},{(hip[1]+fyy)/2:.1f} {hip[0]+w*.28:.1f},{hip[1]-2:.1f} Z" fill="{INK}"/>')
else: # cuerpo afilado
add(f'<path d="M{sx-w/2:.1f},{sy:.1f} Q{sx-w*.55:.1f},{sy+h*.45:.1f} {fx-w*.22:.1f},{fy:.1f} L{fx+w*.22:.1f},{fy:.1f} '
f'Q{sx+w*.55:.1f},{sy+h*.4:.1f} {sx+w/2:.1f},{sy:.1f} Z" fill="{INK}"/>')
if brazo == "arriba": # brazo en alto
hx, hy = sx + h * .14, sy - h * .30
add(f'<path d="M{sx+w*.15:.1f},{sy+h*.04:.1f} Q{sx+h*.12:.1f},{sy-h*.06:.1f} {hx:.1f},{hy:.1f}" stroke="{INK}" stroke-width="{h*.045:.1f}" stroke-linecap="round" fill="none"/>')
add(f'<circle cx="{hx:.1f}" cy="{hy:.1f}" r="{h*.035:.1f}" fill="{INK}"/>')
elif brazo == "atras": # brazo que se balancea hacia atrás
add(f'<path d="M{sx-w*.2:.1f},{sy+h*.05:.1f} Q{sx-h*.12:.1f},{sy+h*.2:.1f} {sx-h*.2:.1f},{sy+h*.32:.1f}" stroke="{INK}" stroke-width="{h*.04:.1f}" stroke-linecap="round" fill="none"/>')
add(f'<ellipse cx="{sx+lean*.15:.1f}" cy="{sy-r*.75:.1f}" rx="{r:.1f}" ry="{r*1.15:.1f}" fill="{INK}"/>')
if random.random() < .15: # estela de movimiento en naranja
for k in range(2):
y = sy + h * (.15 + .22 * k)
add(f'<line x1="{sx-w*1.6:.1f}" y1="{y:.1f}" x2="{sx-w*1.6-h*.16:.1f}" y2="{y+h*.03:.1f}" stroke="{ORA}" stroke-width="{max(1.2,h*.012):.1f}" stroke-linecap="round" opacity=".9"/>')
gente = []
for zd in DOORS:
n = random.randint(7, 10) if zd < 20 else 5
for i in range(n):
t = (i + random.uniform(.1, .9)) / n # 0 en el portal, 1 en la calzada
X = FX + .5 + t * random.uniform(4, 9)
Z = zd + .75 - t * zd * random.uniform(.3, .5)
if Z < 2.6 or any(abs(Z - g[0]) < .35 and abs(X - g[1]) < .45 for g in gente):
continue
paso = random.random() < .3
brazo = random.choices(["arriba", "atras", None], [.15, .2, .65])[0]
gente.append((Z, X, paso, brazo))
gente.sort(reverse=True) # de lejos a cerca
add('<g filter="url(#pintura)">')
for Z, X, paso, brazo in gente:
presencia(X, Z, paso, brazo)
add('</g></svg>')
open(OUT, "w").write("\n".join(o))
print(f"{len(gente)} presencias", file=sys.stderr)