Nuevo dibujo (calle en perspectiva), New Rocker y naranja #ff7a1a

This commit is contained in:
hacklab 2026-10-13 12:52:18 +02:00
parent e8c59a3929
commit df4d68724d
10 changed files with 1690 additions and 389 deletions

View file

@ -1,6 +1,6 @@
# TAKE THE STREET
![¡A la calle!](docs/calle.svg)
![Calle en perspectiva con gente saliendo de los portales](docs/calle.svg)
**takethestreet.net / takethestreet.org**: red internacional de las acampadas por la vivienda que nacieron en la Puerta del Sol el 26 de septiembre de 2026 (26S). La vivienda es un derecho, no un negocio.
@ -114,16 +114,14 @@ Las personas de enlace de cada ciudad no se ponen aquí: van en la sala cerrada
`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), pero con otras tipografías:
- títulos: **Rubik Wet Paint** (pintura que chorrea), OFL
- títulos: **New Rocker** (gótica de cartel de rock, sin pasarse), OFL
- etiquetas: **Special Elite** (máquina de escribir de fanzine), Apache 2.0
- botones: **Anton**, OFL, la misma que 26s
El color es el rojo `#ff1f1f`. El dibujo `docs/calle.svg` se genera con `tools/calle.py`, que convierte el texto de la pancarta a trazos. Para regenerarlo:
El color es el naranja `#ff7a1a`. El dibujo `docs/calle.svg` es una calle en perspectiva con gente saliendo de los portales. La calzada, el bordillo, la acera y el paso de cebra van con línea limpia; los edificios, a mano alzada; y la gente, con trazo suelto de spray. Se genera con `tools/calle.py`, sin dependencias. Para regenerarlo:
```
python3 tools/calle.py RubikWetPaint-Regular.ttf docs/calle.svg
python3 tools/calle.py docs/calle.svg
```
Necesita `fonttools` y la fuente Rubik Wet Paint completa (TTF).
Código AGPL-3.0. Textos y dibujo CC BY-SA 4.0. Tipografías: Rubik Wet Paint y Anton (OFL), Special Elite (Apache 2.0).
Código AGPL-3.0. Textos y dibujo CC BY-SA 4.0. Tipografías: New Rocker y Anton (OFL), Special Elite (Apache 2.0).

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

View file

@ -1,13 +1,13 @@
@font-face { font-family: "Rubik Wet Paint"; src: url("../fonts/rubik-wet-paint.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "New Rocker"; src: url("../fonts/new-rocker.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "Special Elite"; src: url("../fonts/special-elite.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "Anton"; src: url("../fonts/anton.woff2") format("woff2"); font-display: swap; }
:root {
--bg: #fff;
--fg: #000;
--neon: #ff1f1f;
--neon: #ff7a1a;
--bd: 3px solid var(--fg);
--tit: "Rubik Wet Paint", Impact, sans-serif;
--tit: "New Rocker", "Old English Text MT", Georgia, serif;
--zine: "Special Elite", "Courier New", monospace;
--bt: "Anton", Impact, "Arial Narrow", sans-serif;
--txt: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
@ -19,7 +19,7 @@ a { color: inherit; }
.cab {
display: grid;
grid-template-columns: 260px 1fr auto;
grid-template-columns: 340px 1fr auto;
align-items: center;
gap: 20px;
padding: 10px 20px;
@ -28,8 +28,7 @@ a { color: inherit; }
.cab img { width: 100%; height: auto; display: block; }
h1 {
margin: 0;
font: 400 clamp(40px, 6vw, 84px)/1 var(--tit);
text-transform: uppercase;
font: 400 clamp(44px, 6.5vw, 96px)/1 var(--tit);
text-shadow: .03em .03em 0 var(--neon), .06em .06em 0 var(--neon);
}
.lema { margin: 22px 0 0; font: 18px var(--zine); }

View file

@ -1,93 +1,96 @@
Copyright 2020 The Rubik Filtered Project Authors (https://https://github.com/NaN-xyz/Rubik-Filtered)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Copyright (c) 2011, Pablo Impallari (www.impallari.com|impallari@gmail.com),
Copyright (c) 2011, Brenda Gallo (gbrenda1987@gmail.com),
Copyright (c) 2011, Rodrigo Fuenzalida (www.rfuenzalida.com|hello@rfuenzalida.com),
with Reserved Font Name 'New Rocker'
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View file

@ -8,9 +8,9 @@
</head>
<body>
<header class="cab">
<img src="../docs/calle.svg" alt="Gente con una pancarta «¡A la calle!» delante de las tiendas de una acampada">
<img src="../docs/calle.svg" alt="Calle en perspectiva con gente saliendo de los portales">
<div>
<h1>Take the street</h1>
<h1>Take the Street</h1>
<p class="lema">La vivienda es un derecho, no un negocio · red internacional de acampadas</p>
</div>
<nav class="idiomas"><a href="#" aria-current="page">ES</a><a href="#">EN</a><a href="#">PT</a><a href="#">IT</a><a href="#">FR</a></nav>

View file

@ -1,119 +1,156 @@
# Genera docs/calle.svg: ilustración stencil "a la calle"
import sys, random
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.pens.transformPen import TransformPen
random.seed(26)
FONT, OUT = sys.argv[1], sys.argv[2]
# Genera docs/calle.svg: calle en perspectiva, gente saliendo de los portales.
# Calzada y acera con líneas limpias; edificios a mano alzada; gente con trazo difuso.
import sys, random, math
random.seed(1310)
OUT = sys.argv[1]
W, H = 1200, 640
INK, RED = "#000", "#ff1f1f"
out = []
def add(s): out.append(s)
VX, VY, F = 600, 205, 500 # punto de fuga y focal
EYE = 4.0 # cámara a 4 m (desde un balcón)
ROAD, WALK, TOP = 3.4, 6.6, -16.0 # medio ancho calzada, fachada, altura
INK, ORA = "#111", "#ff7a1a"
o = []
add = o.append
def text_path(txt, x, y, size, fill, anchor="middle", font=FONT):
f = TTFont(font); gs = f.getGlyphSet(); cmap = f.getBestCmap()
upm = f["head"].unitsPerEm; k = size / upm
widths = [f["hmtx"][cmap[ord(c)]][0] for c in txt]
total = sum(widths) * k
cx = x - total / 2 if anchor == "middle" else x
d = []
for c, w in zip(txt, widths):
pen = SVGPathPen(gs)
gs[cmap[ord(c)]].draw(TransformPen(pen, (k, 0, 0, -k, cx, y)))
d.append(pen.getCommands()); cx += w * k
return f'<path fill="{fill}" d="{" ".join(d)}"/>'
def blob(cx, cy, r, n, jit, fill, op=1):
import math
pts = []
for i in range(n):
a = 2 * math.pi * i / n
rr = r * (1 + random.uniform(-jit, jit))
if random.random() < .25: rr *= 1.25
pts.append((cx + rr * math.cos(a), cy + rr * math.sin(a)))
d = f"M{pts[0][0]:.0f},{pts[0][1]:.0f} " + " ".join(
f"Q{p[0]:.0f},{p[1]:.0f} {(p[0]+q[0])/2:.0f},{(p[1]+q[1])/2:.0f}"
for p, q in zip(pts, pts[1:] + pts[:1])) + "Z"
add(f'<path d="{d}" fill="{fill}" opacity="{op}"/>')
def splats(cx, cy, spread, n, rmax, fill):
for _ in range(n):
x = cx + random.gauss(0, spread); y = cy + random.gauss(0, spread * .6)
add(f'<circle cx="{x:.0f}" cy="{y:.0f}" r="{random.uniform(2, rmax):.1f}" fill="{fill}"/>')
def drips(x0, x1, y, fill, n):
for _ in range(n):
x = random.uniform(x0, x1); l = random.uniform(15, 70); w = random.uniform(5, 11)
add(f'<path d="M{x-w/2:.0f},{y} v{l:.0f} a{w/2:.1f},{w/2:.1f} 0 0 0 {w:.0f},0 v-{l:.0f}z" fill="{fill}"/>')
def tent(x, y, w, h):
add(f'<path d="M{x},{y} L{x+w/2},{y-h} L{x+w},{y} Z" fill="#fff" stroke="{INK}" stroke-width="8" stroke-linejoin="round"/>')
add(f'<path d="M{x+w/2},{y-h} L{x+w*.38},{y} L{x+w*.62},{y} Z" fill="{INK}"/>')
add(f'<path d="M{x+w/2},{y-h} l0,-22 l26,9 l-26,9" fill="{RED}" stroke="{INK}" stroke-width="4" stroke-linejoin="round"/>')
def fig(x, y, s=1.0, pose="down", step=1):
L = lambda *p: " ".join(f"{a:.0f}" for a in p)
sw = 19 * s
st = f'stroke="{INK}" stroke-width="{sw:.0f}" stroke-linecap="round" stroke-linejoin="round" fill="none"'
hipy = y - 78 * s; shy = y - 150 * s
# piernas (andando)
add(f'<path d="M{L(x-9*s, hipy)} L{L(x-9*s-24*s*step, y-38*s)} L{L(x-12*s-30*s*step, y)}" {st}/>')
add(f'<path d="M{L(x+9*s, hipy)} L{L(x+14*s+16*s*step, y-40*s)} L{L(x+20*s+26*s*step, y)}" {st}/>')
# torso (chupa)
add(f'<path d="M{L(x-31*s, shy)} L{L(x+31*s, shy)} L{L(x+24*s, hipy+6*s)} L{L(x-24*s, hipy+6*s)} Z" fill="{INK}" stroke="{INK}" stroke-width="{8*s:.0f}" stroke-linejoin="round"/>')
# cresta (detrás) + cabeza
if pose in ("fist", "cresta"):
for i in range(5):
dx = (i - 2) * 11 * s
add(f'<path d="M{L(x+dx-8*s, shy-40*s)} L{L(x+dx*1.9, shy-112*s)} L{L(x+dx+8*s, shy-40*s)} Z" fill="{RED}" stroke="{INK}" stroke-width="{5*s:.0f}" stroke-linejoin="round"/>')
add(f'<circle cx="{x:.0f}" cy="{shy-36*s:.0f}" r="{24*s:.0f}" fill="{INK}"/>')
# brazos
ls, rs = (x - 27 * s, shy + 8 * s), (x + 27 * s, shy + 8 * s)
if pose == "fist":
add(f'<path d="M{L(*rs)} L{L(x+52*s, shy-38*s)} L{L(x+50*s, shy-100*s)}" {st}/>')
add(f'<rect x="{x+33*s:.0f}" y="{shy-138*s:.0f}" width="{34*s:.0f}" height="{40*s:.0f}" rx="{10*s:.0f}" fill="{INK}"/>')
add(f'<path d="M{L(*ls)} L{L(x-46*s, shy+50*s)} L{L(x-40*s, shy+88*s)}" {st}/>')
elif pose.startswith("pole"):
side = -1 if pose == "poleL" else 1
px = x + side * 46 * s
add(f'<path d="M{L(*(rs if side>0 else ls))} L{L(px, shy-20*s)}" {st}/>')
add(f'<path d="M{L(*(ls if side>0 else rs))} L{L(x, shy+30*s)} L{L(px, shy+20*s)}" {st}/>')
else:
add(f'<path d="M{L(*ls)} L{L(x-44*s, shy+50*s)} L{L(x-38*s, shy+90*s)}" {st}/>')
add(f'<path d="M{L(*rs)} L{L(x+44*s, shy+50*s)} L{L(x+38*s, shy+90*s)}" {st}/>')
def P(X, Y, Z): # Y hacia abajo, suelo en Y = EYE
return VX + X * F / Z, VY + Y * F / Z
def poly(pts, **a):
d = "M" + " L".join(f"{x:.1f},{y:.1f}" for x, y in pts) + "Z"
add(f'<path d="{d}" ' + " ".join(f'{k.replace("_","-")}="{v}"' for k, v in a.items()) + "/>")
def line(p, q, w, filt=None, col=INK, op=1):
f = f' filter="url(#{filt})"' if filt else ""
add(f'<line x1="{p[0]:.1f}" y1="{p[1]:.1f}" x2="{q[0]:.1f}" y2="{q[1]:.1f}" stroke="{col}" stroke-width="{w:.1f}" stroke-linecap="round" opacity="{op}"{f}/>')
ZN, ZF = 4.4, 400.0
add(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">')
add(f'<rect width="{W}" height="{H}" fill="#fff"/>')
# mancha roja de spray detrás
blob(600, 300, 250, 22, .18, RED)
splats(600, 300, 230, 70, 9, RED)
drips(420, 780, 470, RED, 9)
# calle: asfalto + paso de cebra
GY = 540
add(f'<rect x="0" y="{GY}" width="{W}" height="{H-GY}" fill="{INK}"/>')
for i in range(-2, 14):
x = i * 100
add(f'<path d="M{x+30},{GY+14} L{x+80},{GY+14} L{x+60},{H-14} L{x-10},{H-14} Z" fill="#fff"/>')
# tiendas al fondo
tent(40, GY, 190, 140); tent(190, GY, 140, 100)
tent(930, GY, 200, 150); tent(1070, GY, 120, 90)
# pancarta entre dos palos
bx0, bx1, by = 300, 900, 50
add(f'<path d="M{bx0},{by} L{bx1},{by} L{bx1},{by+115} Q{(bx0+bx1)/2},{by+140} {bx0},{by+115} Z" fill="#fff" stroke="{INK}" stroke-width="9" stroke-linejoin="round"/>')
add(text_path("¡A LA CALLE!", 600, by + 86, 74, INK))
drips(bx0 + 40, bx1 - 40, by + 122, INK, 6)
add(f'<line x1="{bx0}" y1="{by-20}" x2="{bx0}" y2="{GY-150}" stroke="{INK}" stroke-width="12" stroke-linecap="round"/>')
add(f'<line x1="{bx1}" y1="{by-20}" x2="{bx1}" y2="{GY-150}" stroke="{INK}" stroke-width="12" stroke-linecap="round"/>')
# gente
fig(bx0 + 46, GY - 4, 1.05, "poleL", 1)
fig(470, GY - 2, .95, "down", -1)
fig(600, GY, 1.15, "fist", 1)
fig(730, GY - 2, .95, "cresta", -1)
fig(bx1 - 46, GY - 4, 1.05, "poleR", 1)
# estrellas
for (sx, sy, r) in [(120, 90, 26), (1080, 110, 32), (990, 300, 16), (200, 300, 14)]:
import math
pts = " ".join(f"{sx + (r if i%2==0 else r*.42)*math.cos(math.pi/2 + i*math.pi/5):.0f},{sy - (r if i%2==0 else r*.42)*math.sin(math.pi/2 + i*math.pi/5):.0f}" for i in range(10))
add(f'<polygon points="{pts}" fill="{INK}"/>')
add('</svg>')
open(OUT, "w").write("\n".join(out))
add('''<defs>
<filter id="lapiz" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency="0.035" numOctaves="2" seed="3"/><feDisplacementMap in="SourceGraphic" scale="4"/></filter>
<filter id="trazo" x="-30%" y="-30%" width="160%" height="160%"><feTurbulence type="fractalNoise" baseFrequency="0.13" numOctaves="2" seed="5" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="3.5" result="d"/><feGaussianBlur in="d" stdDeviation="0.45"/></filter>
<filter id="spray" x="-30%" y="-30%" width="160%" height="160%"><feTurbulence type="fractalNoise" baseFrequency="0.09" numOctaves="3" seed="8" result="t"/><feDisplacementMap in="SourceGraphic" in2="t" scale="9" result="d"/><feGaussianBlur in="d" stdDeviation="0.7"/></filter>
<radialGradient id="luz"><stop offset="0" stop-color="#ff7a1a" stop-opacity=".9"/><stop offset="1" stop-color="#ff7a1a" stop-opacity="0"/></radialGradient>
<clipPath id="lienzo"><rect width="1200" height="640"/></clipPath>
</defs>''')
add(f'<g clip-path="url(#lienzo)"><rect width="{W}" height="{H}" fill="#fff"/>')
# luz naranja al fondo de la calle
add(f'<ellipse cx="{VX}" cy="{VY-10}" rx="170" ry="120" fill="url(#luz)"/>')
add(f'<g filter="url(#spray)">')
for _ in range(28):
a = random.uniform(0, 2*math.pi); r = random.uniform(30, 190)
add(f'<circle cx="{VX+r*math.cos(a)*1.2:.0f}" cy="{VY-10+r*math.sin(a)*.7:.0f}" r="{random.uniform(1.5,6):.1f}" fill="{ORA}"/>')
add('</g>')
# --- fachadas (a mano alzada) ---
def fachada(s):
X = s * WALK
# línea de cornisas / pisos
for Y in (EYE - 3.2, EYE - 6.8, EYE - 10.4, EYE - 14.0):
line(P(X, Y, ZN), P(X, Y, ZF), 2.2, "lapiz")
# aristas verticales de los portales y ventanas
z = ZN + 1.2
i = 0
while z < 90:
dz = 1.5
# portal
poly([P(X, EYE, z), P(X, EYE - 2.6, z), P(X, EYE - 2.6, z + dz), P(X, EYE, z + dz)],
fill=ORA if i % 2 == 0 else "#fff", stroke=INK, stroke_width=f"{max(1.2, 30/z):.1f}", filter="url(#lapiz)")
if i % 2 == 1:
poly([P(X, EYE, z+.2), P(X, EYE - 2.4, z+.2), P(X, EYE - 2.4, z + dz-.2), P(X, EYE, z + dz-.2)], fill=INK, opacity=".85", filter="url(#lapiz)")
# ventanas encima
for Yb in (EYE - 4.2, EYE - 7.8, EYE - 11.4):
poly([P(X, Yb, z+.15), P(X, Yb - 1.8, z+.15), P(X, Yb - 1.8, z + dz-.15), P(X, Yb, z + dz-.15)],
fill="none", stroke=INK, stroke_width=f"{max(.8, 18/z):.1f}", filter="url(#lapiz)")
z += dz + 2.2 + z * .12
i += 1
fachada(-1); fachada(1)
# --- acera y calzada (líneas limpias) ---
for s in (-1, 1):
# acera (gris claro) y bordillo
poly([P(s*ROAD, EYE-.15, ZN), P(s*WALK, EYE-.15, ZN), P(s*WALK, EYE-.15, ZF), P(s*ROAD, EYE-.15, ZF)], fill="#f2f2f2")
poly([P(s*ROAD, EYE-.15, ZN), P(s*ROAD, EYE, ZN), P(s*ROAD, EYE, ZF), P(s*ROAD, EYE-.15, ZF)], fill=INK)
line(P(s*ROAD, EYE-.15, ZN), P(s*ROAD, EYE-.15, ZF), 3)
line(P(s*WALK, EYE, ZN), P(s*WALK, EYE, ZF), 3)
# juntas de las baldosas
z = ZN
while z < 120:
line(P(s*ROAD, EYE-.15, z), P(s*WALK, EYE-.15, z), max(.6, 9/z))
z *= 1.12
for k in (1, 2):
Xk = s * (ROAD + (WALK - ROAD) * k / 3)
line(P(Xk, EYE-.15, ZN), P(Xk, EYE-.15, ZF), .8, op=.6)
# línea discontinua central
z = ZN
while z < 200:
poly([P(-.08, EYE, z), P(.08, EYE, z), P(.08, EYE, z+2.2), P(-.08, EYE, z+2.2)], fill=INK)
z += 2.2 + 2.6 + z * .08
# bordes de la calzada
for s in (-1, 1):
line(P(s*(ROAD-.25), EYE, ZN), P(s*(ROAD-.25), EYE, ZF), 2.4)
# paso de cebra delante
for i in range(-6, 7):
x0 = i * .52
poly([P(x0-.2, EYE, ZN+.2), P(x0+.2, EYE, ZN+.2), P(x0+.2, EYE, ZN+2.6), P(x0-.2, EYE, ZN+2.6)], fill=INK)
# --- gente (trazo difuso) ---
def persona(X, Z, col=INK, pose=None):
x, y = P(X, EYE, Z)
h = 1.72 * F / Z
if pose is None:
pose = random.choice(["anda", "anda", "puño", "brazos", "anda"])
paso = random.uniform(-1, 1)
w = max(1.1, .042 * h)
hd = (x + random.uniform(-.02, .02) * h, y - .9 * h)
nk, hip = (x, y - .8 * h), (x + paso * .01 * h, y - .48 * h)
sh_l, sh_r = (x - .11 * h, y - .78 * h), (x + .11 * h, y - .78 * h)
pie_l, pie_r = (x - .05*h - paso*.08*h, y), (x + .05*h + paso*.08*h, y)
rod_l, rod_r = (x - .04*h - paso*.04*h, y - .25*h), (x + .04*h + paso*.05*h, y - .25*h)
if pose == "puño":
m_l, c_l = (x - .14*h, y - .46*h), (x - .15*h, y - .62*h)
m_r, c_r = (x + .1*h, y - 1.18*h), (x + .17*h, y - .98*h)
elif pose == "brazos":
m_l, c_l = (x - .26*h, y - 1.12*h), (x - .2*h, y - .95*h)
m_r, c_r = (x + .26*h, y - 1.12*h), (x + .2*h, y - .95*h)
else:
m_l, c_l = (x - .15*h - paso*.05*h, y - .44*h), (x - .16*h, y - .6*h)
m_r, c_r = (x + .15*h + paso*.05*h, y - .44*h), (x + .16*h, y - .6*h)
def tr(*pts, ww):
d = "M" + " L".join(f"{a+random.uniform(-.6,.6):.1f},{b+random.uniform(-.6,.6):.1f}" for a, b in pts)
add(f'<path d="{d}" fill="none" stroke="{col}" stroke-width="{ww:.1f}" stroke-linecap="round" stroke-linejoin="round"/>')
for pas in range(2): # dos pasadas desplazadas = trazo suelto
ww = w * (1 if pas == 0 else .6)
dx, dy = (0, 0) if pas == 0 else (random.uniform(-.03, .03) * h, random.uniform(-.02, .02) * h)
add(f'<g opacity="{.92 if pas == 0 else .45}" transform="translate({dx:.1f},{dy:.1f})">')
tr(nk, hip, ww=ww * 1.5)
tr(hip, rod_l, pie_l, ww=ww)
tr(hip, rod_r, pie_r, ww=ww)
tr(sh_l, c_l, m_l, ww=ww * .85)
tr(sh_r, c_r, m_r, ww=ww * .85)
tr(sh_l, sh_r, ww=ww * 1.2)
add(f'<ellipse cx="{hd[0]:.1f}" cy="{hd[1]:.1f}" rx="{.062*h:.1f}" ry="{.072*h:.1f}" fill="{col}"/>')
add('</g>')
gente = []
# saliendo de los portales (en la acera)
for s in (-1, 1):
z = ZN + 1.2; i = 0
while z < 70:
if i % 2 == 0:
for k in range(random.randint(1, 3)):
gente.append((s * random.uniform(WALK - 1.6, WALK - .4), z + random.uniform(.2, 1.4), INK))
z += 1.5 + 2.2 + z * .12; i += 1
# bajando a la calzada, hacia el centro
for _ in range(42):
Z = math.exp(random.uniform(math.log(6.5), math.log(70)))
gente.append((random.uniform(-ROAD + .4, ROAD - .4), Z, ORA if random.random() < .22 else INK))
gente.sort(key=lambda g: -g[1])
add('<g filter="url(#trazo)">')
for X, Z, col in gente:
persona(X, Z, col)
add('</g>')
# salpicaduras sueltas
add('<g filter="url(#spray)">')
for _ in range(40):
add(f'<circle cx="{random.uniform(0,W):.0f}" cy="{random.uniform(H*.55,H):.0f}" r="{random.uniform(1,3.5):.1f}" fill="{INK}" opacity=".7"/>')
add('</g>')
add('</g></svg>')
open(OUT, "w").write("\n".join(o))