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.
This commit is contained in:
parent
efedefe535
commit
94227e9838
12 changed files with 506 additions and 514 deletions
136
tools/bocetos.py
136
tools/bocetos.py
|
|
@ -1,136 +0,0 @@
|
|||
# Genera docs/bocetos/boceto-1..5.svg. Monigotes de un solo trazo de spray en zigzag,
|
||||
# altos y juntos, en piña: el centro en colores de fuego y alrededor fríos
|
||||
# (a la manera del contenedor y las «Ausencias» de Suso33).
|
||||
import sys, random, math, os
|
||||
OUT = sys.argv[1]
|
||||
W, H = 1600, 700
|
||||
FUEGO = ["#fffbe6", "#ffe066", "#ffb627", "#ff7b1c", "#f04e23", "#c8102e"] # del centro hacia fuera
|
||||
FRIO = ["#5b2a86", "#8e44c9", "#3b5bdb", "#6c7ae0", "#e9e6f5", "#111", "#b07cd8"]
|
||||
|
||||
DEFS = '''<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=".7" result="b"/><feGaussianBlur in="SourceGraphic" stdDeviation="4" result="n"/><feComponentTransfer in="n" result="n2"><feFuncA type="linear" slope=".35"/></feComponentTransfer><feMerge><feMergeNode in="n2"/><feMergeNode in="b"/></feMerge></filter>
|
||||
<filter id="brocha" x="-10%" y="-10%" width="120%" height="120%"><feTurbulence type="fractalNoise" baseFrequency="0.02 0.15" numOctaves="3" seed="4" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="6"/></filter>
|
||||
<filter id="halo" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="40"/></filter>
|
||||
<filter id="sombra"><feGaussianBlur stdDeviation="3"/></filter>
|
||||
</defs>'''
|
||||
|
||||
def fuego(d): # d = 0 en el centro de la piña, 1 en el borde
|
||||
i = min(len(FUEGO) - 1, int(max(0, d + random.uniform(-.12, .12)) * len(FUEGO)))
|
||||
return FUEGO[i]
|
||||
|
||||
def monigote(o, x, y, h, col, lean=0.0, paso=False, brazo=False, sombra=False):
|
||||
"""Un trazo: cabeza en zigzag apretado y cuerpo en zigzag alto. (x, y) = pies."""
|
||||
sw = max(1.1, h * .011)
|
||||
dy = sw * 2.0 # renglones del zigzag
|
||||
if sombra:
|
||||
o.append(f'<path d="M{x-h*.05:.1f},{y:.1f} L{x+h*.6:.1f},{y+h*.14:.1f} L{x+h*.64:.1f},{y+h*.11:.1f} L{x+h*.05:.1f},{y-2:.1f}Z" fill="#111" opacity=".14" filter="url(#sombra)"/>')
|
||||
top = y - h
|
||||
pts = []
|
||||
yy = top
|
||||
i = 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: hombros y 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
|
||||
k = (u - .5) / .5
|
||||
cx += (-1 if (i // 2) % 2 else 1) * h * .035 * k
|
||||
w *= .6
|
||||
s = -1 if i % 2 else 1
|
||||
pts.append((cx + s * 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)
|
||||
o.append(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, n = [], 10
|
||||
for j in range(n):
|
||||
t = j / (n - 1)
|
||||
p.append((bx + t * h * .06 + (-1 if j % 2 else 1) * h * .012, by - t * h * .30))
|
||||
d = "M" + " L".join(f"{px:.1f},{py:.1f}" for px, py in p)
|
||||
o.append(f'<path d="{d}" fill="none" stroke="{col}" stroke-width="{sw:.1f}" stroke-linecap="round" filter="url(#spray)"/>')
|
||||
|
||||
def portal(o, x, y, w, h, fondo="#111"):
|
||||
o.append(f'<g filter="url(#brocha)"><rect x="{x}" y="{y-h}" width="{w}" height="{h}" fill="{fondo}" opacity=".9"/>'
|
||||
f'<path d="M{x-8},{y} L{x-8},{y-h-8} L{x+w+8},{y-h-8} L{x+w+8},{y}" fill="none" stroke="#111" stroke-width="7" stroke-linecap="round"/></g>')
|
||||
|
||||
def svg(cuerpo, fondo="#fff"):
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">{DEFS}'
|
||||
f'<rect width="{W}" height="{H}" fill="{fondo}"/>' + "".join(cuerpo) + "</svg>\n")
|
||||
|
||||
def fila(o, xs, y0, h0, centro, radio, frio=FRIO, lean=0.0, jit=12, hvar=.15, paso=.2, brazo=.08):
|
||||
"""Una piña: monigotes muy juntos; los cercanos al centro, en fuego."""
|
||||
for x in xs:
|
||||
d = abs(x - centro) / radio
|
||||
col = fuego(d) if d < 1 else random.choice(frio)
|
||||
h = h0 * random.uniform(1 - hvar, 1 + hvar) * (1.05 if d < 1 else 1)
|
||||
monigote(o, x, y0 + random.uniform(-jit, jit), h, col, lean, random.random() < paso, random.random() < brazo)
|
||||
|
||||
# 1 · Contenedor: piña de fuego en una pared gris, como el contenedor
|
||||
def b1():
|
||||
random.seed(1); o = []
|
||||
o.append('<circle cx="800" cy="400" r="190" fill="#ffb627" opacity=".35" filter="url(#halo)"/>')
|
||||
xs = sorted(random.uniform(90, 1510) for _ in range(70))
|
||||
fila(o, xs[::2], 610, 470, 800, 260)
|
||||
fila(o, xs[1::2], 650, 500, 800, 230)
|
||||
return o, "#9aa0a8"
|
||||
|
||||
# 2 · Piña que sale a la calle: en blanco, la masa avanza hacia la derecha
|
||||
def b2():
|
||||
random.seed(2); o = []
|
||||
o.append('<g filter="url(#brocha)"><path d="M40,640 L1560,632" stroke="#111" stroke-width="7" stroke-linecap="round"/></g>')
|
||||
xs = sorted(random.gauss(820, 300) for _ in range(55))
|
||||
xs = [x for x in xs if 80 < x < 1520]
|
||||
fila(o, xs, 630, 400, 1000, 230, lean=.12, paso=.45, brazo=.12)
|
||||
return o, "#fff"
|
||||
|
||||
# 3 · El portal: del hueco sale la gente y el fuego va delante
|
||||
def b3():
|
||||
random.seed(3); o = []
|
||||
o.append('<g filter="url(#brocha)"><path d="M40,610 L1560,602" stroke="#111" stroke-width="7" stroke-linecap="round"/></g>')
|
||||
portal(o, 150, 610, 190, 380)
|
||||
o.append('<circle cx="330" cy="430" r="130" fill="#ffb627" opacity=".35" filter="url(#halo)"/>')
|
||||
for i in range(30):
|
||||
t = i / 29
|
||||
x = 250 + t * 1250 + random.uniform(-25, 25)
|
||||
d = abs(t - .32) / .25
|
||||
col = fuego(d) if d < 1 else random.choice(FRIO)
|
||||
monigote(o, x, 610 + t * 70 + random.uniform(-8, 8), 300 + t * 120, col, .1, random.random() < .45, random.random() < .12)
|
||||
return o, "#fff"
|
||||
|
||||
# 4 · Calle en diagonal: la piña baja por la calle hacia quien mira
|
||||
def b4():
|
||||
random.seed(4); o = []
|
||||
o.append('<g filter="url(#brocha)"><path d="M40,690 L1180,300" stroke="#111" stroke-width="9" stroke-linecap="round"/>'
|
||||
'<path d="M330,700 L1260,305" stroke="#f04e23" stroke-width="3" stroke-linecap="round" opacity=".9"/></g>')
|
||||
ps = []
|
||||
for _ in range(1500):
|
||||
t = random.random()
|
||||
x = 1240 - t * 700 + random.gauss(0, 40 + t * 150)
|
||||
y = 300 + t * 390 + random.gauss(0, 10)
|
||||
if 290 < y < 695 and all(math.hypot(x - a, (y - b) * 3) > 22 + t * 50 for a, b, _ in ps):
|
||||
ps.append((x, y, t))
|
||||
for x, y, t in sorted(ps, key=lambda p: p[1]):
|
||||
d = math.hypot(x - 820, (y - 520) * 1.5) / 230
|
||||
col = fuego(d) if d < 1 else random.choice(FRIO)
|
||||
monigote(o, x, y, 60 + t * 230, col, -.1, random.random() < .35, random.random() < .1)
|
||||
return o, "#fff"
|
||||
|
||||
# 5 · Ausencias: todos en negro y gris, y solo el corazón de la piña en fuego
|
||||
def b5():
|
||||
random.seed(5); o = []
|
||||
xs = sorted(random.uniform(100, 1500) for _ in range(42))
|
||||
fila(o, xs, 650, 430, 900, 120, frio=["#111", "#111", "#333", "#555", "#888"], lean=.06, hvar=.12, paso=.3, brazo=.1)
|
||||
return o, "#fff"
|
||||
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
for n, f in enumerate([b1, b2, b3, b4, b5], 1):
|
||||
cuerpo, fondo = f()
|
||||
open(f"{OUT}/boceto-{n}.svg", "w").write(svg(cuerpo, fondo))
|
||||
194
tools/calle.py
194
tools/calle.py
|
|
@ -1,111 +1,111 @@
|
|||
# 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.
|
||||
# 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(26)
|
||||
random.seed(4)
|
||||
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"
|
||||
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): # Yh = altura sobre el suelo
|
||||
def P(X, Yh, Z):
|
||||
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}">')
|
||||
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="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>
|
||||
<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>''')
|
||||
|
||||
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>')
|
||||
# --- 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>')
|
||||
|
||||
# --- 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"/>')
|
||||
# --- 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 = []
|
||||
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>')
|
||||
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)} presencias", file=sys.stderr)
|
||||
print(f"{len(gente)} monigotes", file=sys.stderr)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue