317 lines
13 KiB
Python
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())
|