takethestreet/tools/plazas.py
2026-10-14 12:01:13 +02:00

317 lines
13 KiB
Python

# Genera la portada (la plaza que nace del cruce del boceto 5) y los bocetos 6 a 10:
# plazas y cruces con el mismo acabado a mano que tools/bocetos.py.
import sys, random, math, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from bocetos import Calle, INK, FUEGO, FRIO, LUCHAS
EJES = ["#f04e23", "#14965a", "#f2b705", "#3b5bdb", "#8e44c9"]
class Escena(Calle):
def zn(s):
return s.EYE * s.F / (s.H + s.Y0 - s.VY) * .8
def seg(s, a, b, w=None, op=.95):
zn = s.zn()
if a[2] < zn and b[2] < zn:
return
if a[2] > b[2]:
a, b = b, a
if a[2] < zn:
t = (zn - a[2]) / (b[2] - a[2])
a = tuple(a[i] + (b[i] - a[i]) * t for i in range(3))
if w is None:
w = 34 / ((a[2] + b[2]) / 2) + 1.6
s.linea(s.P(*a), s.P(*b), w, op)
def poli(s, pts, w=None, op=.95):
for a, b in zip(pts, pts[1:]):
s.seg(a, b, w, op)
def abrir(s, *mascaras):
for m in mascaras:
s.o.append(f'<g mask="url(#{m})">')
s.o.append('<g filter="url(#pincel)" fill="none">')
s.cierre = '</g>' + '</g>' * len(mascaras)
def cerrar(s):
s.o.append(s.cierre)
def portal(s, a, b, alto=2.5, cornisa=True):
s.poli([(a[0], 0, a[1]), (a[0], alto, a[1]), (b[0], alto, b[1]), (b[0], 0, b[1])])
if cornisa:
dx, dz = b[0] - a[0], b[1] - a[1]
s.seg((a[0] - dx * .2, alto + .35, a[1] - dz * .2), (b[0] + dx * .2, alto + .35, b[1] + dz * .2), None, .6)
def fachada(s, a, b, portales=(), alto=2.5):
s.seg((a[0], 0, a[1]), (b[0], 0, b[1]))
L = math.hypot(b[0] - a[0], b[1] - a[1])
ux, uz = (b[0] - a[0]) / L, (b[1] - a[1]) / L
for d in portales:
s.portal((a[0] + ux * d, a[1] + uz * d), (a[0] + ux * (d + 1.5), a[1] + uz * (d + 1.5)), alto)
def esquina(s, X, Z, alto=3.2):
s.seg((X, 0, Z), (X, alto, Z), None, .7)
def farola(s, X, Z, h=4.2):
s.seg((X, 0, Z), (X, h, Z), None, .9)
s.seg((X, h, Z), (X + .6, h - .1, Z), None, .9)
c = s.P(X + .6, h - .35, Z)
s.o.append(f'<circle cx="{c[0]:.1f}" cy="{c[1]:.1f}" r="{90 / Z:.1f}" fill="#ffe066" opacity=".55" filter="url(#halo)"/>')
def tienda(s, X, Z, col, a=2.0, f=2.2, alto=1.2):
x0, x1, xm = X - a / 2, X + a / 2, X
fr = [(x0, 0, Z), (xm, alto, Z), (x1, 0, Z)]
s.poli(fr, None, .9)
s.seg((xm, alto, Z), (xm, alto, Z + f), None, .6)
s.seg((x0, 0, Z), (x0, 0, Z + f), None, .5)
s.seg((x1, 0, Z), (x1, 0, Z + f), None, .5)
p = [s.P(*q) for q in fr]
d = f"M{p[0][0]:.1f},{p[0][1]:.1f} L{p[1][0]:.1f},{p[1][1]:.1f} L{p[2][0]:.1f},{p[2][1]:.1f} Z"
s.tiendas.append(f'<path d="{d}" fill="{col}" opacity=".55" filter="url(#spray)"/>')
def gente(s, pina, regueros, extra=(), puños=.1, zona=None, zmax=40, ejes=None):
CX, CZ, sx, sz, n, R = pina
zmin = s.EYE * s.F / (s.H + s.Y0 - s.VY) + .3
zona = zona or (lambda X, Z: True)
g = []
def cabe(X, Z):
return all(math.hypot(X - a, (Z - b) * .8) > .42 + .022 * Z for a, b, _ in g)
for X0, Z0, col, k in regueros:
for j in range(k):
t = (j + .5) / k
X = X0 + (CX - X0) * t + random.uniform(-.3, .3)
Z = Z0 + (CZ - Z0) * t + random.uniform(-.3, .3)
if Z > zmin and cabe(X, Z):
g.append((X, Z, col))
for _ in range(n):
X, Z = random.gauss(CX, sx), random.gauss(CZ, sz)
if zmin < Z < zmax and zona(X, Z) and cabe(X, Z):
g.append((X, Z, None))
g += list(extra)
c = s.P(CX, 1.0, CZ)
r0 = 2600 / CZ
s.o.append(f'<circle cx="{c[0]:.0f}" cy="{c[1]:.0f}" r="{r0:.0f}" fill="#ffb627" opacity=".14" filter="url(#halo)"/>')
for _ in range(700):
ang, rr = random.uniform(0, 2 * math.pi), abs(random.gauss(0, r0 * .55))
k = min(len(FUEGO) - 1, int(rr / r0 * len(FUEGO)))
s.o.append(f'<circle cx="{c[0] + math.cos(ang) * rr * 1.3:.1f}" cy="{c[1] + math.sin(ang) * rr * .8:.1f}" r="{random.uniform(.6, 2.6):.1f}" fill="{FUEGO[k]}" opacity="{random.uniform(.15, .5):.2f}"/>')
s.o += s.tiendas
for X, Z, col in sorted(g, key=lambda q: -q[1]):
x, y = s.P(X, 0, Z)
h = 1.7 * s.F / Z * random.uniform(.9, 1.08)
d = math.hypot(X - CX, Z - CZ) / R
if d < 1:
col = FUEGO[min(len(FUEGO) - 1, int(max(0, d + random.uniform(-.12, .12)) * len(FUEGO)))]
elif col is None:
col = random.choice(ejes or FRIO) if ejes and random.random() < .35 else random.choice(FRIO)
s.monigote(x, y, h, col, random.uniform(-.06, .04), random.random() < .35, random.random() < puños)
return len(g)
def __init__(s, **k):
super().__init__(**k)
s.tiendas = []
def llegan(X0, Z0, col, n):
return (X0, Z0, col, n)
def portada():
# El cruce del boceto 5 se abre en una plaza: la calle llega desde abajo y la gente entra por todas partes
random.seed(15)
c = Escena(VX=1000, VY=60, EYE=9.0, F=600, Y0=100)
FA, FD, KI, KD = -6.5, 6.6, -4.2, 3.6
z1, z2, xl, xr = 11.0, 25.0, -24.0, 17.0
c.abrir("lados", "arriba")
c.fachada((FA, 3.0), (FA, z1), (4.0,))
c.fachada((FD, z1), (FD, 3.0), (3.0,))
c.seg((KI, 0, 3.0), (KI, 0, z1 - 1), 9)
c.seg((KI + .18, 0, 3.0), (KI + .18, 0, z1 - 1), 3, .7)
c.seg((KD, 0, 3.0), (KD, 0, z1 - 1), 7, .8)
for z in (4.0, 9.0):
c.seg((-.3, 0, z), (-.3, 0, z + 2.2), None, .8)
c.esquina(FA, z1); c.esquina(FD, z1)
c.fachada((FA, z1), (xl, z1), (6.0, 12.0))
c.fachada((xr, z1), (FD, z1), (4.0,))
c.fachada((xl, z1), (xl, 15.5), ())
c.fachada((xl, 19.5), (xl, z2), ())
for z in (15.5, 19.5):
c.esquina(xl, z)
c.seg((xl, 0, z), (-70, 0, z))
c.fachada((xr, z2), (xr, z1), (3.0, 9.0))
c.fachada((xl, z2), (-4.5, z2), (4.0, 10.0, 15.0))
c.fachada((4.5, z2), (xr, z2), (3.0, 8.0))
for x in (-4.5, 4.5):
c.esquina(x, z2)
c.seg((x, 0, z2), (x, 0, 90))
c.esquina(xl, z1); c.esquina(xr, z1); c.esquina(xl, z2); c.esquina(xr, z2)
for x, z in ((-8, 13.5), (8, 13.5), (-8, 22.5), (8, 22.5)):
c.farola(x, z)
c.cerrar()
c.tienda(-19.5, 12.5, "#14965a")
c.tienda(-16.0, 21.0, "#f2b705")
c.tienda(13.0, 12.8, "#3b5bdb")
c.tienda(13.5, 20.0, "#8e44c9")
zona = lambda X, Z: (KI - 1.4 < X < KD - .3 and Z < z1) or (xl + .8 < X < xr - .8 and z1 + .5 < Z < z2 - .5) or (abs(X) < 4 and Z >= z2)
c.gente((-1.0, 16.0, 3.4, 2.8, 4200, 3.4),
[llegan(FA + .5, 4.75, LUCHAS[0], 6), llegan(-60, 17.5, EJES[1], 14),
llegan(-.3, 60, EJES[3], 12), llegan(xr - 1, 16, EJES[2], 6), llegan(.3, 3.2, EJES[0], 9)],
zona=zona, ejes=EJES, puños=.14, zmax=80)
return c
def b6():
# La plaza: de frente, fachadas en U y la gente llenándola
random.seed(21)
c = Escena(VX=800, VY=120, EYE=6.0, F=560, Y0=80)
zf, xl, xr = 26.0, -15.0, 15.0
c.abrir("lados", "arriba")
c.fachada((xl, 3.0), (xl, zf), (5.0, 10.0, 15.0))
c.fachada((xr, zf), (xr, 3.0), (4.0, 9.0, 14.0))
c.fachada((xl, zf), (-4.0, zf), (2.0, 6.0))
c.fachada((4.0, zf), (xr, zf), (2.5, 6.5))
for x in (-4.0, 4.0):
c.esquina(x, zf)
c.seg((x, 0, zf), (x, 0, 70))
c.esquina(xl, zf); c.esquina(xr, zf)
for x in (-10.0, 10.0):
c.farola(x, 9.0); c.farola(x, 20.0)
c.cerrar()
c.gente((0, 15, 3.2, 2.6, 3500, 3.0),
[llegan(0, 40, LUCHAS[0], 10), llegan(xl + .6, 8.0, LUCHAS[1], 5), llegan(xr - .6, 7.0, LUCHAS[3], 5)],
zona=lambda X, Z: xl + .8 < X < xr - .8 and Z < zf - .5 or abs(X) < 3.5, ejes=EJES, puños=.15)
return c
def b7():
# Dos calles que se cruzan en X: de cada esquina llega un eje con su color
random.seed(22)
c = Escena(VX=800, VY=110, EYE=7.0, F=560, Y0=80)
cz, a = 16.0, 4.5
r = math.sqrt(.5)
def W(u, v):
return ((u - v) * r, (u + v) * r + cz)
c.abrir("lados", "arriba")
for su in (-1, 1):
for sv in (-1, 1):
p0 = W(su * a, sv * a)
pu = W(su * 60, sv * a)
pv = W(su * a, sv * 60)
c.esquina(*p0)
for q in (pu, pv):
c.seg((p0[0], 0, p0[1]), (q[0], 0, q[1]))
for d in (6.0, 12.0):
for (u, v) in ((su * (a + d), sv * a), (su * a, sv * (a + d))):
x, z = W(u, v)
x2, z2 = W(u + su * 1.5 * (v == sv * a), v + sv * 1.5 * (u == su * a))
c.portal((x, z), (x2, z2))
for k in range(-8, 9, 3):
for u, v in ((k, 0), (0, k)):
if abs(k) > a + 1:
p, q = W(u, v), W(u + (1.6 if v == 0 else 0), v + (1.6 if u == 0 else 0))
c.seg((p[0], 0, p[1]), (q[0], 0, q[1]), None, .7)
c.cerrar()
llegadas = []
for (u, v), col in zip(((-30, 0), (30, 0), (0, -9), (0, 30)), EJES):
x, z = W(u, v)
llegadas.append(llegan(x, z, col, 12))
zona = lambda X, Z: abs((X / r + (Z - cz) / r) / 2) < a - .3 or abs(((Z - cz) / r - X / r) / 2) < a - .3
c.gente((0, cz, 1.8, 1.8, 2500, 2.6), llegadas, zona=zona, puños=.15)
return c
def b8():
# La acampada: tiendas de colores en la plaza y la asamblea en el centro
random.seed(23)
c = Escena(VX=800, VY=120, EYE=6.5, F=560, Y0=80)
zf, xl, xr = 28.0, -16.0, 16.0
c.abrir("lados", "arriba")
c.fachada((xl, 3.0), (xl, zf), (6.0, 12.0, 18.0))
c.fachada((xr, zf), (xr, 3.0), (5.0, 11.0, 17.0))
c.fachada((xl, zf), (xr, zf), (4.0, 9.0, 14.0, 19.5, 25.0))
c.esquina(xl, zf); c.esquina(xr, zf)
c.farola(-6, 10); c.farola(6, 10); c.farola(0, 24)
c.cerrar()
random.seed(230)
tiendas = []
for fila, z in enumerate((10.5, 13.0, 22.0, 24.8)):
for x in range(-13, 14, 4):
if abs(x) > 5 or z > 20:
tiendas.append((x + random.uniform(-.6, .6) + (fila % 2) * 2, z + random.uniform(-.4, .4)))
for k, (x, z) in enumerate(sorted(tiendas, key=lambda t: -t[1])):
c.tienda(x, z, EJES[k % len(EJES)])
lejos = lambda X, Z: all(math.hypot(X - x, Z - (z + 1.1)) > 1.6 for x, z in tiendas)
c.gente((0, 17, 2.6, 1.8, 3000, 2.6), [], zona=lambda X, Z: xl < X < xr and Z < zf - .5 and lejos(X, Z), ejes=EJES, puños=.2)
return c
def b9():
# Sol: cinco calles llegan a la misma plaza, cada una con su reguero
random.seed(24)
c = Escena(VX=800, VY=120, EYE=8.0, F=560, Y0=80)
cz, R = 22.0, 11.0
angs = [-70, -35, 0, 35, 70]
c.abrir("lados", "arriba")
pts = []
for k in range(-90, 91, 6):
t = math.radians(k)
pts.append((R * math.sin(t), cz + R * math.cos(t) * .9))
huecos = [math.radians(a) for a in angs]
tramo = []
for k, (x, z) in zip(range(-90, 91, 6), pts):
dentro = any(abs(math.radians(k) - h) < math.radians(8) for h in huecos)
if dentro:
if len(tramo) > 1:
s0 = [(px, 0, pz) for px, pz in tramo]
c.poli(s0)
tramo = []
else:
tramo.append((x, z))
if len(tramo) > 1:
c.poli([(px, 0, pz) for px, pz in tramo])
for h in huecos:
for d in (-1, 1):
t = h + d * math.radians(8)
x0, z0 = R * math.sin(t), cz + R * math.cos(t) * .9
x1, z1 = x0 + math.sin(h) * 40, z0 + math.cos(h) * 40
c.esquina(x0, z0)
c.seg((x0, 0, z0), (x1, 0, z1))
c.seg((-R, 0, cz), (-R, 0, 4)); c.seg((R, 0, cz), (R, 0, 4))
c.farola(0, cz - 3)
c.cerrar()
llegadas = [llegan(math.sin(math.radians(a)) * (R + 14), cz + math.cos(math.radians(a)) * (R + 14) * .9, col, 10)
for a, col in zip(angs, EJES)]
c.gente((0, cz - 1, 2.8, 2.6, 3500, 3.0), llegadas, zona=lambda X, Z: math.hypot(X, (Z - cz) / .9) < R - .6 or Z < cz, ejes=EJES, puños=.15, zmax=60)
return c
def b10():
# Desde el balcón: la plaza vista desde arriba, la piña como una hoguera de gente
random.seed(25)
c = Escena(VX=800, VY=40, EYE=16.0, F=520, Y0=100)
zf, xl, xr = 34.0, -18.0, 18.0
c.abrir("lados")
c.fachada((xl, 8.0), (xl, zf), (8.0, 16.0))
c.fachada((xr, zf), (xr, 8.0), (6.0, 14.0))
c.fachada((xl, zf), (-4.0, zf), (3.0, 9.0))
c.fachada((4.0, zf), (xr, zf), (3.0, 9.0))
for x in (-4.0, 4.0):
c.seg((x, 0, zf), (x, 0, 80))
for x, z in ((-12, 14), (12, 14), (-12, 28), (12, 28)):
c.farola(x, z)
c.cerrar()
c.gente((0, 21, 3.6, 3.2, 4500, 3.8),
[llegan(0, 60, EJES[3], 12), llegan(xl + .6, 12, EJES[1], 6), llegan(xr - .6, 30, EJES[2], 6)],
zona=lambda X, Z: (xl + .8 < X < xr - .8 and Z < zf - .5) or abs(X) < 3.5, ejes=EJES, puños=.12, zmax=70)
return c
if __name__ == "__main__":
bocetos, plaza = sys.argv[1:3]
os.makedirs(bocetos, exist_ok=True)
for n, f in zip(range(6, 11), [b6, b7, b8, b9, b10]):
open(f"{bocetos}/boceto-{n}.svg", "w").write(f().svg())
open(plaza, "w").write(portada().svg())