docs/bocetos/boceto-1..5.svg y su generador tools/bocetos.py: contenedor, la piña sale a la calle, el portal, calle en diagonal y ausencias.
136 lines
7.5 KiB
Python
136 lines
7.5 KiB
Python
# 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))
|