From 2cece9947ff166cd7141b44a9c37f348d3096516 Mon Sep 17 00:00:00 2001 From: hacklab Date: Tue, 13 Oct 2026 13:47:10 +0200 Subject: [PATCH] Cinco bocetos de la calle con acabado a mano y regueros de colores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/bocetos.py: líneas con pulso y doble pasada, zigzag irregular con pasada fantasma y chorreones, niebla de spray en el fuego y grano de pared. De cada portal sale un reguero de un color hacia la piña. --- README.md | 24 +++++ docs/bocetos/boceto-1.svg | 11 ++ docs/bocetos/boceto-2.svg | 11 ++ docs/bocetos/boceto-3.svg | 11 ++ docs/bocetos/boceto-4.svg | 11 ++ docs/bocetos/boceto-5.svg | 11 ++ tools/bocetos.py | 205 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 284 insertions(+) create mode 100644 docs/bocetos/boceto-1.svg create mode 100644 docs/bocetos/boceto-2.svg create mode 100644 docs/bocetos/boceto-3.svg create mode 100644 docs/bocetos/boceto-4.svg create mode 100644 docs/bocetos/boceto-5.svg create mode 100644 tools/bocetos.py diff --git a/README.md b/README.md index 565aacf..2fbb439 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,30 @@ La gente va en color, en una piña que llena la calzada. Cada monigote es un sol python3 tools/calle.py docs/calle.svg ``` +### Bocetos + +Cinco variantes de la calle, con acabado de pintado a mano (líneas con pulso y doble pasada, zigzag irregular, chorreones, niebla de spray y grano de pared). De cada portal sale un reguero de un color, una lucha distinta, y todos se juntan en la piña de fuego. Se generan con `python3 tools/bocetos.py docs/bocetos`. + +1. **La calle**: regueros de colores desde cada portal hacia la piña. + + ![Boceto 1](docs/bocetos/boceto-1.svg) + +2. **A pie de calle**: cámara más baja, la gente cerca y grande. + + ![Boceto 2](docs/bocetos/boceto-2.svg) + +3. **Hacia la luz**: la piña arde al fondo y los regueros bajan hacia ella. + + ![Boceto 3](docs/bocetos/boceto-3.svg) + +4. **De frente**: la piña viene hacia quien mira, con más puños en alto. + + ![Boceto 4](docs/bocetos/boceto-4.svg) + +5. **El cruce**: una calle corta la nuestra y por ella también llega gente. + + ![Boceto 5](docs/bocetos/boceto-5.svg) + ## 7. Maqueta `mockup/index.html` es HTML y CSS, sin JavaScript. Se abre directamente en el navegador. Sigue el estilo de acampada26s.net (blanco y negro, bordes de 3 px, sombra de color): firma en la cabecera, portada con el lema y el dibujo debajo, y debajo el deck.Capturas al principio de este README. diff --git a/docs/bocetos/boceto-1.svg b/docs/bocetos/boceto-1.svg new file mode 100644 index 0000000..3d09b4e --- /dev/null +++ b/docs/bocetos/boceto-1.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/bocetos/boceto-2.svg b/docs/bocetos/boceto-2.svg new file mode 100644 index 0000000..d652200 --- /dev/null +++ b/docs/bocetos/boceto-2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/bocetos/boceto-3.svg b/docs/bocetos/boceto-3.svg new file mode 100644 index 0000000..812be38 --- /dev/null +++ b/docs/bocetos/boceto-3.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/bocetos/boceto-4.svg b/docs/bocetos/boceto-4.svg new file mode 100644 index 0000000..b252aa3 --- /dev/null +++ b/docs/bocetos/boceto-4.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/bocetos/boceto-5.svg b/docs/bocetos/boceto-5.svg new file mode 100644 index 0000000..1666765 --- /dev/null +++ b/docs/bocetos/boceto-5.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tools/bocetos.py b/tools/bocetos.py new file mode 100644 index 0000000..349a76a --- /dev/null +++ b/tools/bocetos.py @@ -0,0 +1,205 @@ +# Genera docs/bocetos/boceto-1..5.svg: la calle en líneas negras y la gente en color, +# en monigotes de un trazo de spray en zigzag (a la manera de Suso33). +# De cada portal sale un reguero de un color (cada lucha) y todos se juntan en la piña, +# que en el centro arde en colores de fuego. +import sys, random, math, os +OUT = sys.argv[1] +INK = "#111" +FUEGO = ["#fffbe6", "#ffe066", "#ffb627", "#ff7b1c", "#f04e23", "#c8102e"] # del centro hacia fuera +LUCHAS = ["#8e44c9", "#3b5bdb", "#14965a", "#d6336c", "#2a9d8f", "#5b2a86"] # un color por reguero +FRIO = ["#5b2a86", "#8e44c9", "#3b5bdb", "#6c7ae0", "#b07cd8", "#2a9d8f", "#111"] + +DEFS = ''' + + + + + + + + + +''' + +class Calle: + def __init__(s, W=1600, H=640, VX=1180, VY=110, F=520, EYE=5.0, FACH=-6.5, KI=-4.2, KD=3.6, Y0=0): + s.__dict__.update(locals()); del s.__dict__["s"] + s.o = [] + def P(s, X, Yh, Z): + return s.VX + X * s.F / Z, s.VY + (s.EYE - Yh) * s.F / Z + def linea(s, a, b, w, op=.95): + """Trazo a mano: temblor suave, grosor que cambia y una segunda pasada.""" + L = math.hypot(b[0] - a[0], b[1] - a[1]) + if L < 1: + return + nx, ny = -(b[1] - a[1]) / L, (b[0] - a[0]) / L + n = max(3, int(L / 40)) + for pasada in range(2): + off, drift = 0.0, random.uniform(-1, 1) * w * .4 + pts = [] + for k in range(n + 1): + t = k / n + off += random.uniform(-1, 1) * w * .35 + off *= .8 + q = off + drift * math.sin(math.pi * t) + pts.append((a[0] + (b[0] - a[0]) * t + nx * q, a[1] + (b[1] - a[1]) * t + ny * q)) + ww = w * (1 if pasada == 0 else .45) + # grosor variable: el trazo se parte en tramos con anchos distintos + for k in range(0, n, 2): + seg = pts[k:k + 3] + d = "M" + " L".join(f"{x:.1f},{y:.1f}" for x, y in seg) + s.o.append(f'') + + def dibujar_calle(s, portales, cruce=None): + P, L = s.P, s.linea + zn = s.EYE * s.F / (s.H + s.Y0 - s.VY) * .8 # lo más cerca que sale en el cuadro + s.o.append('') + tramos = [(zn, 90)] if not cruce else [(zn, cruce[0]), (cruce[1], 90)] + for z0, z1 in tramos: + L(P(s.FACH, 0, z0), P(s.FACH, 0, z1), 7) # pie de la fachada + L(P(s.KI, 0, z0), P(s.KI, 0, z1), 9) # bordillo + L(P(s.KI + .18, 0, z0), P(s.KI + .18, 0, z1), 3, .7) + L(P(s.KD, 0, max(zn, 5)), P(s.KD, 0, 90), 6, .8) # bordillo de enfrente + if cruce: # calle que cruza: bordillos hacia la izquierda + for z in cruce: + L(P(s.KI, 0, z), P(-40, 0, z), 8) + L(P(s.FACH, 0, z), P(s.FACH, 0, z), 1) + z = max(zn, 4.5) + while z < 60: # línea discontinua del centro + L(P(-.3, 0, z), P(-.3, 0, z + 2.2), 2 + 16 / z, .8) + z += 5 + for zd in portales: # portales: solo el contorno + a, b = P(s.FACH, 0, zd), P(s.FACH, 0, zd + 1.5) + c, d = P(s.FACH, 2.5, zd), P(s.FACH, 2.5, zd + 1.5) + w = 30 / zd + 2 + L(a, c, w); L(c, d, w); L(d, b, w) + L(P(s.FACH, 2.85, zd - .3), P(s.FACH, 2.85, zd + 1.8), w * .6, .6) + s.o.append('') + + def monigote(s, 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: + w = h * .05 * math.sin(math.pi * t / .13) ** .6 + sw * .3 + elif t < .16: + w = h * .008 + else: + u = (t - .16) / .84 + w = h * (.062 * (1 - u) ** .8 + .014) + if paso and u > .5: + 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(.7, 1.3) + random.uniform(-.6, .6) * 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) + s.o.append(f'') + gx, gy = random.uniform(-1, 1) * sw, random.uniform(.3, 1) * sw # segunda pasada, fantasma + d2 = "M" + " L".join(f"{px + gx:.1f},{py + gy:.1f}" for px, py in pts[::2]) + s.o.append(f'') + if random.random() < .22 and h > 60: # chorreón + px, py = random.choice(pts[len(pts) // 3: -2]) + s.o.append(f'') + s.o.append(f'') + if brazo: + 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) + s.o.append(f'') + + def gente(s, pina, regueros, extra=(), puños=.1, zmin=None): + """pina = (CX, CZ, sx, sz, n, R): centro, dispersión, intentos y radio del fuego (m). + regueros = [(X0, Z0, color, n)]: salen de (X0, Z0) hacia el centro de la piña.""" + CX, CZ, sx, sz, n, R = pina + zmin = zmin or s.EYE * s.F / (s.H + s.Y0 - s.VY) + .3 + 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: # primero los regueros + for j in range(k): + t = (j + .5) / k + X = X0 + (CX - X0) * t + random.uniform(-.25, .25) + 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): # luego la piña + X, Z = random.gauss(CX, sx), random.gauss(CZ, sz) + if s.KI - 1.6 < X < s.KD - .3 and zmin < Z < 34 and cabe(X, Z): + g.append((X, Z, None)) + for X, Z, col in extra: + g.append((X, Z, col)) + c = s.P(CX, 1.0, CZ) + r0 = 2600 / CZ + s.o.append(f'') + for _ in range(700): # niebla de spray + 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'') + 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(FRIO) + s.monigote(x, y, h, col, random.uniform(-.06, .04), random.random() < .35, random.random() < puños) + return len(g) + + def svg(s): + return (f'{DEFS}' + + "".join(s.o) + f'' + "\n") + +def regueros(c, portales, n=7): + return [(c.FACH + .5, zd + .75, LUCHAS[i % len(LUCHAS)], n) for i, zd in enumerate(portales)] + +# 1 · La calle mejorada: regueros de colores desde cada portal hacia la piña +def b1(): + random.seed(11); c = Calle(Y0=100) + pts = (4.0, 7.5, 11.5, 17.5, 26.0) + c.dibujar_calle(pts) + c.gente((-.3, 11, 2.1, 3.6, 9000, 2.8), regueros(c, pts[:4])) + return c + +# 2 · A pie de calle: cámara más baja, la gente cerca y grande +def b2(): + random.seed(12); c = Calle(EYE=3.0, VY=170, F=560, Y0=60) + pts = (5.0, 9.0, 14.0, 21.0, 32.0) + c.dibujar_calle(pts) + c.gente((.2, 9.5, 2.2, 3.5, 9000, 2.6), regueros(c, pts[:4], 6), puños=.18) + return c + +# 3 · Hacia la luz: la piña arde al fondo y los regueros bajan de los portales hacia ella +def b3(): + random.seed(13); c = Calle(Y0=100) + pts = (4.0, 7.0, 10.5, 15.0, 21.0, 29.0) + c.dibujar_calle(pts) + c.gente((.2, 19, 1.8, 4, 7000, 3.6), regueros(c, pts[:5], 9)) + return c + +# 4 · De frente: la piña viene hacia quien mira, con más puños en alto +def b4(): + random.seed(14); c = Calle(EYE=4.0, VX=1100, VY=130, F=560, Y0=80) + pts = (4.5, 8.0, 12.0, 18.0, 27.0) + c.dibujar_calle(pts) + c.gente((.3, 7.5, 2.4, 2.6, 9000, 2.4), regueros(c, pts[1:4], 6), puños=.28) + return c + +# 5 · El cruce: una calle corta la nuestra y de ella también llega gente +def b5(): + random.seed(15); c = Calle(Y0=100) + pts = (4.0, 7.0, 17.0, 24.0) + cruce = (10.0, 14.0) + c.dibujar_calle(pts, cruce) + lateral = [(-14 + k * 1.3, 12 + random.uniform(-1.2, 1.2), "#14965a" if k % 2 else "#d6336c") for k in range(8)] + c.gente((-.5, 12.5, 2.2, 3.5, 9000, 2.8), regueros(c, (4.0, 7.0, 17.0)), extra=lateral) + return c + +os.makedirs(OUT, exist_ok=True) +for n, f in enumerate([b1, b2, b3, b4, b5], 1): + open(f"{OUT}/boceto-{n}.svg", "w").write(f().svg())