Cinco bocetos de la calle con acabado a mano y regueros de colores
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.
This commit is contained in:
parent
94227e9838
commit
2cece9947f
7 changed files with 284 additions and 0 deletions
24
README.md
24
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.
|
||||
|
||||

|
||||
|
||||
2. **A pie de calle**: cámara más baja, la gente cerca y grande.
|
||||
|
||||

|
||||
|
||||
3. **Hacia la luz**: la piña arde al fondo y los regueros bajan hacia ella.
|
||||
|
||||

|
||||
|
||||
4. **De frente**: la piña viene hacia quien mira, con más puños en alto.
|
||||
|
||||

|
||||
|
||||
5. **El cruce**: una calle corta la nuestra y por ella también llega gente.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
|
|
|||
11
docs/bocetos/boceto-1.svg
Normal file
11
docs/bocetos/boceto-1.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 275 KiB |
11
docs/bocetos/boceto-2.svg
Normal file
11
docs/bocetos/boceto-2.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 300 KiB |
11
docs/bocetos/boceto-3.svg
Normal file
11
docs/bocetos/boceto-3.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 230 KiB |
11
docs/bocetos/boceto-4.svg
Normal file
11
docs/bocetos/boceto-4.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 259 KiB |
11
docs/bocetos/boceto-5.svg
Normal file
11
docs/bocetos/boceto-5.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 295 KiB |
205
tools/bocetos.py
Normal file
205
tools/bocetos.py
Normal file
|
|
@ -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 = '''<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=".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="grano" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="3" seed="7"/><feColorMatrix values="0 0 0 0 0.1 0 0 0 0 0.1 0 0 0 0 0.1 0 0 0 -1.4 .62"/></filter>
|
||||
<filter id="pincel" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency="0.04 0.3" numOctaves="3" seed="4" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="4" result="d"/><feGaussianBlur in="SourceGraphic" stdDeviation="3" result="n"/><feComponentTransfer in="n" result="n2"><feFuncA type="linear" slope=".25"/></feComponentTransfer><feMerge><feMergeNode in="n2"/><feMergeNode in="d"/></feMerge></filter>
|
||||
<filter id="halo" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="50"/></filter>
|
||||
<linearGradient id="gd" x1="0" y1="0" x2="1" y2="0"><stop offset=".62" stop-color="#fff"/><stop offset=".98" stop-color="#000"/></linearGradient>
|
||||
<linearGradient id="gv" x1="0" y1="0" x2="0" y2="1"><stop offset=".02" stop-color="#000"/><stop offset=".25" stop-color="#fff"/></linearGradient>
|
||||
<mask id="derecha" maskUnits="userSpaceOnUse" x="0" y="0" width="1600" height="900"><rect width="1600" height="900" fill="url(#gd)"/></mask>
|
||||
<mask id="arriba" maskUnits="userSpaceOnUse" x="0" y="0" width="1600" height="900"><rect width="1600" height="900" fill="url(#gv)"/></mask>
|
||||
</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'<path d="{d}" stroke="{INK}" stroke-width="{ww * random.uniform(.6, 1.25):.1f}" stroke-linecap="round" stroke-linejoin="round" opacity="{op * (1 if pasada == 0 else .45):.2f}"/>')
|
||||
|
||||
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('<g mask="url(#derecha)"><g mask="url(#arriba)"><g filter="url(#pincel)" fill="none">')
|
||||
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('</g></g></g>')
|
||||
|
||||
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'<path d="{d}" fill="none" stroke="{col}" stroke-width="{sw:.1f}" stroke-linejoin="round" stroke-linecap="round" filter="url(#spray)"/>')
|
||||
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'<path d="{d2}" fill="none" stroke="{col}" stroke-width="{sw * .6:.1f}" stroke-linejoin="round" opacity=".35" filter="url(#spray)"/>')
|
||||
if random.random() < .22 and h > 60: # chorreón
|
||||
px, py = random.choice(pts[len(pts) // 3: -2])
|
||||
s.o.append(f'<path d="M{px:.1f},{py:.1f} q{random.uniform(-1, 1):.1f},{h * .12:.1f} 0,{h * random.uniform(.15, .35):.1f}" stroke="{col}" stroke-width="{sw * .55:.1f}" stroke-linecap="round" fill="none" opacity=".75"/>')
|
||||
s.o.append(f'<circle cx="{px:.1f}" cy="{py + h * .3:.1f}" r="{sw * .45:.1f}" fill="{col}" opacity=".6"/>')
|
||||
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'<path d="{d}" fill="none" stroke="{col}" stroke-width="{sw:.1f}" stroke-linecap="round" filter="url(#spray)"/>')
|
||||
|
||||
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'<circle cx="{c[0]:.0f}" cy="{c[1]:.0f}" r="{r0:.0f}" fill="#ffb627" opacity=".14" filter="url(#halo)"/>')
|
||||
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'<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}"/>')
|
||||
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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 {s.Y0} {s.W} {s.H}" width="{s.W}" height="{s.H}">{DEFS}'
|
||||
+ "".join(s.o) + f'<rect y="{s.Y0}" width="{s.W}" height="{s.H}" filter="url(#grano)" opacity=".5"/>' + "</svg>\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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue