File:Le Modulor.png
Commons is a freely licensed media file repository. You can help.
Summary
| DescriptionLe Modulor.png |
English: Cover for Le Corbusier's Modulor |
| Date | |
| Source | Own work |
| Author | Xeno4n |
The code to generate this image is import math import re import xml.etree.ElementTree as ET
import numpy as np import svgwrite
- ============================================================
- Parameters
- ============================================================
phi = (1 + math.sqrt(5)) / 2 t, T = 0.0075, 0.75 W = (phi + 1) / 2
x_left, x_right = -W + 0.25, W + 0.25 ymin, ymax = -phi - 1, 0.0 A = 0.25 * (1 - 1 / phi)
- Outer bounds
x_bg_min, x_bg_max = x_left - T, x_right + T y_bg_min, y_bg_max = ymin - T, ymax + T
- ============================================================
- Palette (closer to the scanned cover: warmer greys, orange-red, off-white paper)
- ============================================================
PAPER = "#F0F2F0" # off-white (book/paper) BG_GREY = "#CFD1CB" # warm light grey field MID_GREY = "#BCBCBC" # medium grey (vertical stripe block base) CHARCOAL = "#1A1A18" RED = "#DF3917" # brighter orange-red YELLOW = "#F4F131" # warmer yellow TITLE_DARK_YELLOW = "#CDBF3A" # shadow/ink edge under yellow
SALMON_BASE = "#EBCCB1" SALMON = "#D06939" # warm orange-red dots # salmon dots WHITE = PAPER # keep your existing WHITE name but make it match paper
- ============================================================
- Typography (closer to cover: heavy + manually condensed)
- ============================================================
- ============================================================
- ONE size knob only:
- K = number of SCALE LEVELS (bars), and waves use 2 lobes per level
- ============================================================
K = 50 NLOBES = 2 * K
- Prefix list used in m(x) and envelope placement (indexed by lobe index n)
prefixes = [] prefix = 0.0 for n in range(NLOBES + 3): # padding
prefixes.append(prefix) prefix += 1.0 / (phi ** (n // 2))
- Shared y-grid for smooth curves
YS = np.linspace(ymin, ymax, 3000)
- ============================================================
- SVG setup
- ============================================================
dwg = svgwrite.Drawing(
"inequalities.svg",
size=("200mm", "200mm"),
viewBox=f"{x_bg_min} {y_bg_min} {x_bg_max - x_bg_min} {y_bg_max - y_bg_min}",
)
- ------------------------------------------------------------
- Embed local fonts (deterministic SVG -> PNG via CairoSVG)
- Put these .ttf files next to your script:
- BebasNeue-Regular.ttf
- ArchivoNarrow-SemiBold.ttf
- ------------------------------------------------------------
import base64
def _ttf_to_data_uri(ttf_path: str) -> str:
with open(ttf_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
return f"data:font/ttf;base64,{b64}"
FONT_MODULOR_NAME = "SairaCondensedSemiBold"
saira_uri = _ttf_to_data_uri("SairaCondensed-SemiBold.ttf")
dwg.defs.add(dwg.style(f""" @font-face {{
font-family: '{FONT_MODULOR_NAME}';
src: url('{saira_uri}') format('truetype');
}} """))
TITLE_FONT_FAMILY = (f"{FONT_MODULOR_NAME}, Arial, sans-serif") TITLE_FONT_WEIGHT = "800" TITLE_LETTER_SPACING = "0em"
- ------------------------------------------------------------
- STRIPE PATTERN (for formerly-blue regions)
- thinner black bars per your request
- ------------------------------------------------------------
stripe_h = 0.02 * 26 / 32 * 30 / 23 stripe_band = stripe_h * 0.18 # thinner than before
pat = dwg.pattern(
id="hstripes", insert=(0, 0), size=(1.0, stripe_h), patternUnits="userSpaceOnUse",
)
- white base of stripes (use PAPER, not pure white)
pat.add(dwg.rect(insert=(0, 0), size=(1.0, stripe_h), fill=PAPER, stroke="none"))
- thin black bar
pat.add(dwg.rect(insert=(0, 0), size=(1.0, stripe_band), fill=CHARCOAL, stroke="none")) dwg.defs.add(pat)
STRIPED = "url(#hstripes)"
- ------------------------------------------------------------
- VERTICAL STRIPE PATTERN (very thin, very close)
- ------------------------------------------------------------
vstripe_pitch = 0.015 / 2 * 34 / 30 # distance between stripes (smaller = closer) vstripe_band = vstripe_pitch * 0.3 # very thin line vpat = dwg.pattern(
id="vstripes", insert=(0, 0), size=(vstripe_pitch, 1.0), patternUnits="userSpaceOnUse",
)
- base (use BG_GREY so it blends with background)
vpat.add(dwg.rect(insert=(0, 0), size=(vstripe_pitch, 1.0), fill=MID_GREY, stroke="none"))
- thin vertical line
vpat.add(dwg.rect(insert=(0, 0), size=(vstripe_band, 1.0), fill=CHARCOAL, stroke="none")) dwg.defs.add(vpat)
VSTRIPED = "url(#vstripes)"
- Flip y-axis for geometry; keep text unflipped via helper functions
g = dwg.g(transform=f"translate(0,{y_bg_min + y_bg_max}) scale(1,-1)") dwg.add(g)
- ------------------------------------------------------------
- DOT PATTERNS (small, dense halftone style)
- ------------------------------------------------------------
- spacing between dots
dot_r = 0.002 * 30 / 19 # dot radius (small) dot_pitch = 3.5 * dot_r
- --- GREY DOTS with 45° rotation ---
grey_pat = dwg.pattern(
id="grey_dots", insert=(0, 0), size=(dot_pitch, dot_pitch), patternUnits="userSpaceOnUse", patternTransform="rotate(45)" # <-- rotate the dots
)
grey_pat.add(dwg.rect((0, 0), (dot_pitch, dot_pitch), fill=BG_GREY, stroke="none")) grey_pat.add(dwg.circle(
center=(dot_pitch / 2, dot_pitch / 2), r=dot_r, fill=CHARCOAL, stroke="none"
)) dwg.defs.add(grey_pat) GREY_DOTTED = "url(#grey_dots)"
- --- SALMON DOTS with 45° rotation ---
salmon_pat = dwg.pattern(
id="salmon_dots", insert=(0, 0), size=(dot_pitch, dot_pitch), patternUnits="userSpaceOnUse", patternTransform="rotate(45)" # <-- rotate the dots
)
salmon_pat.add(dwg.rect((0, 0), (dot_pitch, dot_pitch), fill=SALMON_BASE, stroke="none")) salmon_pat.add(dwg.circle(
center=(dot_pitch / 2, dot_pitch / 2), r=dot_r, fill=SALMON, stroke="none"
)) dwg.defs.add(salmon_pat) SALMON_DOTTED = "url(#salmon_dots)"
- ============================================================
- Helpers
- ============================================================
def rect(x1, x2, y1, y2, fill):
xa, xb = sorted([x1, x2]) ya, yb = sorted([y1, y2]) g.add(dwg.rect((xa, ya), (xb - xa, yb - ya), fill=fill, stroke="none"))
def poly(pts, fill):
g.add(dwg.polygon(pts, fill=fill, stroke="none"))
def fill_curve_to_vertical(xs, ys, x_const, fill):
pts = list(zip(xs, ys)) + [(x_const, float(y)) for y in ys[::-1]] poly(pts, fill)
def fill_between_curves(xs_left, xs_right, ys, fill):
pts = list(zip(xs_left, ys)) + list(zip(xs_right[::-1], ys[::-1])) poly(pts, fill)
def add_label_model(x_model, y_model, text, fill=CHARCOAL, size=0.025):
y_svg = (y_bg_min + y_bg_max) - y_model
dwg.add(dwg.text(
text, insert=(x_model, y_svg),
text_anchor="middle",
dominant_baseline="alphabetic",
fill=fill, font_size=size, font_family="Arial"
))
def add_label_model_rot(x_model, y_model, text, angle_deg=90, fill=CHARCOAL, size=0.025):
y_svg = (y_bg_min + y_bg_max) - y_model
txt = dwg.text(
text, insert=(x_model, y_svg),
text_anchor="middle",
dominant_baseline="middle",
fill=fill, font_size=size, font_family="Arial"
)
txt.rotate(angle_deg, center=(x_model, y_svg))
dwg.add(txt)
def draw_salmon_base_to_bottom(y_cutoff):
# Fill salmon between the 2nd vertical and the center line down to ymin x_second_vertical = -0.25 - t / 2 rect(x_second_vertical, -t / 2, ymin, y_cutoff, SALMON_DOTTED)
- ============================================================
- Modulor SVG inserter
- ============================================================
def add_title_modulor(x_model, y_model, text, angle_deg=-90, size=0.4):
""" Title text (single-layer, no shadow). """ y_svg = (y_bg_min + y_bg_max) - y_model
txt = dwg.text(
text,
insert=(x_model, y_svg),
text_anchor="middle",
dominant_baseline="middle",
fill=YELLOW,
font_size=size,
font_family=TITLE_FONT_FAMILY,
font_weight=TITLE_FONT_WEIGHT,
)
txt.attribs["letter-spacing"] = TITLE_LETTER_SPACING
cx, cy = x_model, y_svg
txt.attribs["transform"] = (
f"translate({cx},{cy}) "
f"rotate({angle_deg}) "
f"scale(0.96,1.18) "
f"translate({-cx},{-cy})"
)
# IMPORTANT: actually add it to the SVG dwg.add(txt)
def add_modulor_outline(
svg_path, *, x_touch, x_panel_min, y_panel_min, y_panel_max, stroke=CHARCOAL, stroke_width=0.02, include_holes=True, fill_interior=True, fill_color=CHARCOAL
):
ns = {"svg": "http://www.w3.org/2000/svg"}
root = ET.parse(svg_path).getroot()
def parse_points(points_str):
return [(float(x), float(y))
for x, y in re.findall(r'(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)', points_str or "")]
polylines = root.findall(".//svg:polyline", ns)
polygons = root.findall(".//svg:polygon", ns)
if len(polylines) < 2:
return
# Outer silhouette = polyline0 + polyline1 joined
pts0 = parse_points(polylines[0].attrib.get("points", ""))
pts1 = parse_points(polylines[1].attrib.get("points", ""))
outer = pts0 + pts1[1:] # avoid duplicate join point
# Holes
holes = [parse_points(pg.attrib.get("points", "")) for pg in polygons] if include_holes else []
# BBox
allpts = outer[:]
for hpts in holes:
allpts.extend(hpts)
xs = [p[0] for p in allpts]
ys_ = [p[1] for p in allpts]
xmin, xmax = min(xs), max(xs)
ymin_s, ymax_s = min(ys_), max(ys_)
h = ymax_s - ymin_s
if h <= 0:
return
# elbow-ish x (upper arm band) y0 = ymin_s + 0.12 * h y1 = ymin_s + 0.28 * h elbow_candidates = [(x, y) for (x, y) in allpts if y0 <= y <= y1] elbow_x = max(elbow_candidates, key=lambda p: p[0])[0] if elbow_candidates else xmax
sy = (y_panel_max - y_panel_min) / h denom = (elbow_x - xmin) if (elbow_x - xmin) != 0 else (xmax - xmin) sx_limit = (x_touch - x_panel_min) / denom sx = min(sx_limit, sy)
x0m = x_touch - (elbow_x - xmin) * sx y_bottom = y_panel_min
def xf(x): return x0m + (x - xmin) * sx def yf(y): return y_bottom + (ymax_s - y) * sy
def subpath(pts):
if not pts:
return ""
d = f"M {xf(pts[0][0])} {yf(pts[0][1])} "
for x, y in pts[1:]:
d += f"L {xf(x)} {yf(y)} "
d += "Z "
return d
if fill_interior:
d = subpath(outer) + "".join(subpath(hp) for hp in holes)
g.add(dwg.path(d=d, fill=fill_color, stroke="none", fill_rule="evenodd"))
g.add(dwg.path(d=subpath(outer), fill="none", stroke=stroke,
stroke_width=stroke_width, stroke_linecap="round", stroke_linejoin="round"))
for hp in holes:
g.add(dwg.path(d=subpath(hp), fill="none", stroke=stroke,
stroke_width=stroke_width, stroke_linecap="round", stroke_linejoin="round"))
- ============================================================
- Core wave functions
- ============================================================
def gfun(x):
return 1.0 if 0 <= x <= 1 else 0.0
def h(u):
return math.cos(math.pi * u) if abs(u) < 0.5 else 0.0
def m(x):
total, pref = 0.0, 0.0
for n in range(NLOBES):
a = 1.0 / (phi ** (n // 2))
denom = a - 2.0 * t
if denom <= 0:
break
u = (2.0 * x - pref - (denom / 2.0)) / denom
total += ((-1.0) ** n) * h(u) * denom
pref += 1.0 / (phi ** (n // 2))
return total
- ============================================================
- Envelopes
- ============================================================
- how far to move envelope curves inward toward x=0
ENVELOPE_INWARD = 0 # max meaningful brings baselines to x=0
def draw_first_envelope_remove_first_wave(*, stroke=CHARCOAL, stroke_width=0.003, npts=900,
fill_gap=True, draw_stroke=True): # height H = 1 - t/2, top-anchored at y=ymax H = 1.0 - (t / 2.0) y_top = ymax y_bot = y_top - H
y_center = 0.5 * (y_top + y_bot) ys_loc = np.linspace(y_bot, y_top, npts) u = (ys_loc - y_center) / H shape = np.where(np.abs(u) < 0.5, np.abs(np.cos(np.pi * u)), 0.0)
s = H / 0.5 amp = s off = A * amp * shape
xr_env = (t / 2.0) + off
# background fill (must be UNDER waves)
if fill_gap:
x_edge = np.full_like(ys_loc, t / 2)
xL = np.minimum(xr_env, x_edge)
xR = np.maximum(xr_env, x_edge)
fill_between_curves(xL, xR, ys_loc, WHITE)
# stroke (can be drawn later ON TOP)
if draw_stroke:
g.add(dwg.polyline(
list(zip(xr_env, ys_loc)),
fill="none", stroke=stroke, stroke_width=stroke_width,
stroke_linecap="round", stroke_linejoin="round",
))
def stroke_curve(xs, ys, stroke=CHARCOAL, stroke_width=t):
g.add(dwg.polyline(
list(zip(xs, ys)),
fill="none",
stroke=stroke,
stroke_width=stroke_width,
stroke_linecap="round",
stroke_linejoin="round",
))
def add_pair_envelopes_rhs_only(*, stroke=CHARCOAL, stroke_width=0.003, npts=800,
fill_gap=True, draw_stroke=True):
for k in range(1, K):
a = 1.0 / (phi ** k)
d = a - 2.0 * t
if d <= 0:
break
n0 = 2 * k
p0 = prefixes[n0]
y_top = -t / 2 - (p0 / 2.0)
y_bot = -t / 2 - ((p0 + a + d) / 2.0)
span = y_top - y_bot
if span <= 0:
continue
s = (d + t) / (d / 2.0)
amp = s * d
y_center = 0.5 * (y_top + y_bot)
ys_loc = np.linspace(y_bot, y_top, npts)
u = (ys_loc - y_center) / span
shape = np.where(np.abs(u) < 0.5, np.abs(np.cos(np.pi * u)), 0.0)
off = A * amp * shape
xr_env = (t / 2.0) + off
# background fill (must be UNDER waves)
if fill_gap:
x_edge = np.full_like(ys_loc, t / 2)
xL = np.minimum(xr_env, x_edge)
xR = np.maximum(xr_env, x_edge)
fill_between_curves(xL, xR, ys_loc, WHITE)
# stroke (can be drawn later ON TOP)
if draw_stroke:
g.add(dwg.polyline(
list(zip(xr_env, ys_loc)),
fill="none", stroke=stroke, stroke_width=stroke_width,
stroke_linecap="round", stroke_linejoin="round",
))
- NOTE: this will use xl_outer computed later (global) for salmon strip
xl_outer = None
def add_left_sliding_envelopes(*, stroke=CHARCOAL, stroke_width=0.003, npts=900):
lowest_y = ymax
"""
Draw LEFT-side envelopes and fill SALMON between:
(second vertical line from left) x = -0.25 - t/2
and the envelope curve.
"""
x_second_vertical = -0.25 - t / 2 # <-- the line you mean
for k in range(0, max(0, K - 1)):
a_k = 1.0 / (phi ** k)
a_k1 = 1.0 / (phi ** (k + 1))
d_k = a_k - 2.0 * t
d_k1 = a_k1 - 2.0 * t
if d_k <= 0 or d_k1 <= 0:
break
xk = d_k / 2.0
n1 = 2 * k + 1
n2 = 2 * k + 2
if n2 >= len(prefixes):
break
p1 = prefixes[n1]
p2 = prefixes[n2]
y_top_1 = -t / 2 - (p1 / 2.0)
y_bot_2 = -t / 2 - ((p2 + d_k1) / 2.0)
span = y_top_1 - y_bot_2
if span <= 0:
continue
s = span / xk
amp = s * d_k
y_center = 0.5 * (y_top_1 + y_bot_2)
ys_loc = np.linspace(y_bot_2, y_top_1, npts)
lowest_y = min(lowest_y, float(np.min(ys_loc)))
u = (ys_loc - y_center) / span
shape = np.where(np.abs(u) < 0.5, np.abs(np.cos(np.pi * u)), 0.0)
off = A * amp * shape
# envelope curve (near the middle)
xl_env = (-t / 2.0 + ENVELOPE_INWARD) - off
# VERTICAL-STRIPED fill between LEFT waves boundary and the envelope
# xl_outer is the outer red wave boundary sampled on global YS.
# Interpolate it to this envelope's local ys grid:
# SALMON (dotted) fill between the 2nd vertical line and the envelope
x_second_vertical = -0.25 - t / 2 # the 2nd vertical line from the left
xL = np.minimum(np.full_like(ys_loc, x_second_vertical), xl_env)
xR = np.maximum(np.full_like(ys_loc, x_second_vertical), xl_env)
fill_between_curves(xL, xR, ys_loc, SALMON_DOTTED)
# OPTIONAL: vertical stripes between LEFT waves boundary and the envelope
x_outer = np.interp(ys_loc, YS, xl_outer)
xL2 = np.minimum(x_outer, xl_env)
xR2 = np.maximum(x_outer, xl_env)
mask2 = (xR2 - xL2) > 1e-6
if np.any(mask2):
fill_between_curves(xL2[mask2], xR2[mask2], ys_loc[mask2], VSTRIPED)
# draw envelope stroke
g.add(dwg.polyline(
list(zip(xl_env, ys_loc)),
fill="none", stroke=stroke, stroke_width=stroke_width,
stroke_linecap="round", stroke_linejoin="round"
))
if lowest_y > ymin + 1e-6:
draw_salmon_base_to_bottom(lowest_y)
def draw_left_top_envelope_and_salmon(*, stroke=CHARCOAL, stroke_width=0.003, npts=900,
fill_salmon=True, fill_vstripes=True, draw_stroke=True,
y_mirror=-0.5):
"""
Build the TOP left envelope by mirroring the first (k=0) left sliding envelope
across the horizontal divider y = y_mirror (blue line).
"""
# --- reconstruct the k=0 envelope exactly as in add_left_sliding_envelopes ---
k = 0
a_k = 1.0 / (phi ** k) # = 1
a_k1 = 1.0 / (phi ** (k + 1)) # = 1/phi
d_k = a_k - 2.0 * t
d_k1 = a_k1 - 2.0 * t
if d_k <= 0 or d_k1 <= 0:
return
xk = d_k / 2.0
n1 = 2 * k + 1
n2 = 2 * k + 2
if n2 >= len(prefixes):
return
p1 = prefixes[n1] p2 = prefixes[n2]
y_top_1 = -t / 2 - (p1 / 2.0)
y_bot_2 = -t / 2 - ((p2 + d_k1) / 2.0)
span = y_top_1 - y_bot_2
if span <= 0:
return
s = span / xk amp = s * d_k
y_center = 0.5 * (y_top_1 + y_bot_2) ys_low = np.linspace(y_bot_2, y_top_1, npts) u = (ys_low - y_center) / span shape = np.where(np.abs(u) < 0.5, np.abs(np.cos(np.pi * u)), 0.0) off = A * amp * shape
# lower envelope (k=0) xl_env_low = (-t / 2.0 + ENVELOPE_INWARD) - off
# --- mirror across the horizontal divider y = y_mirror --- ys_top = 2.0 * y_mirror - ys_low xl_env_top = xl_env_low.copy()
# --- stretch by factor phi in BOTH x and y, anchored at bottom cusp --- y_anchor = float(np.min(ys_top)) idx_anchor = np.argmin(ys_top) x_anchor = float(xl_env_top[idx_anchor])
# scale both coordinates about anchor point ys_scaled = y_anchor + 1.15 * (ys_top - y_anchor) xs_scaled = x_anchor + 1.15 * (xl_env_top - x_anchor)
ys_loc = ys_scaled xl_env = xs_scaled
# keep increasing y order order = np.argsort(ys_loc) ys_loc = ys_loc[order] xl_env = xl_env[order]
# keep ys increasing for cleaner polylines / fills order = np.argsort(ys_loc) ys_loc = ys_loc[order] xl_env = xl_env[order]
# SALMON fill between 2nd vertical line and envelope
if fill_salmon:
x_second_vertical = -0.25 - t / 2
xL = np.full_like(ys_loc, x_second_vertical)
xR = xl_env
mask = (xR - xL) > 1e-6
if np.any(mask):
fill_between_curves(xL[mask], xR[mask], ys_loc[mask], SALMON_DOTTED)
# VSTRIPED fill between OUTER red boundary and envelope (like lower ones)
if fill_vstripes:
x_outer = np.interp(ys_loc, YS, xl_outer)
xL2 = np.minimum(x_outer, xl_env)
xR2 = np.maximum(x_outer, xl_env)
mask2 = (xR2 - xL2) > 1e-6
if np.any(mask2):
fill_between_curves(xL2[mask2], xR2[mask2], ys_loc[mask2], VSTRIPED)
# Stroke
if draw_stroke:
g.add(dwg.polyline(
list(zip(xl_env, ys_loc)),
fill="none", stroke=stroke, stroke_width=stroke_width,
stroke_linecap="round", stroke_linejoin="round",
))
- ============================================================
- Bars (constant thickness, constant font)
- ============================================================
def l1(n): return -((((1 - phi ** (n + 1)) / (1 - phi)) - 0.5) / (phi ** n)) def l2(n): return -(((1 - phi ** (n + 1)) / (1 - phi)) / (phi ** n))
def draw_bars_and_labels():
bar_ht = t / 2 font_size = 0.045 label_offset = 0.018
for n in range(K):
# l2 bar (right of center)
y = l2(n)
xL, xR = t / 2, 0.25 - t / 2
rect(xL, xR, y - bar_ht, y + bar_ht, CHARCOAL)
# l1 bar (left of center)
y = l1(n)
xL, xR = -0.25 + t / 2, -t / 2
rect(xL, xR, y - bar_ht, y + bar_ht, CHARCOAL)
- ============================================================
- Draw
- ============================================================
def stroke_vertical_baseline_segment(*, x, y0, y1, stroke=CHARCOAL, stroke_width=t):
g.add(dwg.line(
start=(x, y0),
end=(x, y1),
stroke=stroke,
stroke_width=stroke_width,
stroke_linecap="round",
))
- Background first
rect(x_bg_min, x_bg_max, y_bg_min, y_bg_max, GREY_DOTTED)
- --- RHS envelope background (WHITE) goes UNDER waves ---
add_pair_envelopes_rhs_only(stroke=CHARCOAL, stroke_width=t, fill_gap=True, draw_stroke=False) draw_first_envelope_remove_first_wave(stroke=CHARCOAL, stroke_width=t, fill_gap=True, draw_stroke=False)
- ----------------------------
- Middle waves (fills)
- Formerly BLUE -> STRIPED
- Formerly RED stays RED
- ----------------------------
xr19 = [A * abs(gfun(((-y - 0.5) / (phi + 0.5))) * m(-t / 2 - y)) + t / 2 for y in YS] xl19 = [-A * abs(gfun(((-y - 0.5) / (phi + 0.5))) * m(-t / 2 - y)) - t / 2 for y in YS] fill_curve_to_vertical(xr19, YS, t / 2, STRIPED) fill_curve_to_vertical(xl19, YS, -t / 2, RED)
def nfun(x): return h((2 * x - 0.5 + t / 2) * 0.5 / (0.5 - t / 2))
xr24 = [A * abs(nfun(-y)) + t / 2 for y in YS]
xl24 = [-A * abs(nfun(-y)) - t / 2 for y in YS]
fill_curve_to_vertical(xr24, YS, t / 2, STRIPED)
fill_curve_to_vertical(xl24, YS, -t / 2, RED)
def stroke_curve_skip_baseline(xs, ys, *, baseline_x, eps=1e-6, stroke=CHARCOAL, stroke_width=t):
""" Stroke only the parts of (xs,ys) where xs is not essentially equal to baseline_x. This removes the long vertical bar that happens when xs == baseline_x over large spans. """ xs = np.asarray(xs, dtype=float) ys = np.asarray(ys, dtype=float)
keep = np.abs(xs - baseline_x) > eps
if not np.any(keep):
return
# draw each contiguous kept segment as its own polyline idx = np.where(keep)[0] breaks = np.where(np.diff(idx) > 1)[0]
starts = np.r_[idx[0], idx[breaks + 1]] ends = np.r_[idx[breaks], idx[-1]]
for a, b in zip(starts, ends):
seg = list(zip(xs[a:b + 1], ys[a:b + 1]))
if len(seg) >= 2:
g.add(dwg.polyline(
seg,
fill="none",
stroke=stroke,
stroke_width=stroke_width,
stroke_linecap="round",
stroke_linejoin="round",
))
- black outlines around the central wave boundaries
stroke_curve(xr19, YS, stroke=CHARCOAL, stroke_width=t) stroke_curve_skip_baseline(xl19, YS, baseline_x=-t / 2, eps=1e-6, stroke=CHARCOAL, stroke_width=2 * t)
stroke_curve(xr24, YS, stroke=CHARCOAL, stroke_width=t) stroke_curve_skip_baseline(xl24, YS, baseline_x=-t / 2, eps=1e-6, stroke=CHARCOAL, stroke_width=2 * t)
- This is the "vertical curve just to the left" of the left envelopes:
- use the OUTER red wave boundary across all y
xl_outer = np.minimum(np.array(xl19), np.array(xl24))
def add_box_label(
x_center, y_center, text, w, h, fill=WHITE, stroke=CHARCOAL, stroke_width=0.01, font_family="Arial Narrow, Helvetica Neue Condensed, Helvetica, Arial, sans-serif", font_weight="800", y_stretch=1.12, letter_spacing_em=0.06, width_k=0.8, pad_frac=0.20, # horizontal padding fraction pad_y_frac=0.10 # vertical padding fraction
):
"""
Boxed label with:
- font size auto-fit to BOTH height and width (approx model)
- clipPath so text cannot overflow the box
- vertical stretch of glyphs
Keeps your dots unchanged; only affects label rendering. """
# --- draw box fill + stroke in model coords (group g) ---
rect(x_center - w / 2, x_center + w / 2, y_center - h / 2, y_center + h / 2, fill)
g.add(dwg.rect(
insert=(x_center - w / 2, y_center - h / 2),
size=(w, h),
fill="none",
stroke=stroke,
stroke_width=stroke_width
))
# --- compute a safe font size --- # height limit (uppercase fits ~0.64*h comfortably) h_avail = h * (1.0 - pad_y_frac) fs_h = 0.92 * h_avail
# width limit (approx): text_width ≈ (n_chars * k * fs) + (n_gaps * letter_spacing)
n_chars = len(text.replace(" ", "")) # ignore spaces for width estimate
n_spaces = text.count(" ")
# treat space as ~0.45 glyph
eff_chars = n_chars + 0.45 * n_spaces
# available width after padding w_avail = w * (1.0 - pad_frac)
# convert letter spacing from em to model units gaps = max(0, len(text) - 1) denom = (eff_chars * width_k + gaps * letter_spacing_em) fs_w = (w_avail / denom) if denom > 1e-9 else fs_h
font_size = min(fs_h, fs_w)
# --- clip path in UNFLIPPED SVG space ---
y_svg = (y_bg_min + y_bg_max) - y_center
clip_id = f"clip_{abs(hash((x_center, y_center, text))) % 10**9}"
clip = dwg.clipPath(id=clip_id)
clip.add(dwg.rect(
insert=(x_center - w / 2, y_svg - h / 2),
size=(w, h),
fill="none"
))
dwg.defs.add(clip)
# --- draw text (unflipped) clipped to the box ---
txt = dwg.text(
text,
insert=(x_center, y_svg),
text_anchor="middle",
alignment_baseline="central",
fill=stroke,
font_size=font_size,
font_family=font_family,
font_weight=font_weight,
)
txt.attribs["letter-spacing"] = f"{letter_spacing_em}em"
txt.attribs["clip-path"] = f"url(#{clip_id})"
# vertical stretch about center
cx, cy = x_center, y_svg
txt.attribs["transform"] = (
f"translate({cx},{cy}) "
f"scale(1,{y_stretch}) "
f"translate({-cx},{-cy})"
)
dwg.add(txt)
- ----------------------------
- Leftmost drops (formerly BLUE) -> YELLOW
- ----------------------------
def x_from_y(y):
xb = x_left
a1 = A * math.sin((-y * math.pi) / (0.5 - t / 2)) * gfun((-y) / (0.5 - t / 2)) if y >= -0.5 + t / 2 else 0.0
denom2 = (phi / 2) - t
z2 = (-y - (t + 1) / 2)
a2 = A * math.sin((math.pi / denom2) * z2) * gfun(z2 / denom2)
stretch = W / (W - t / 2)
a3 = A * math.sin((2 * math.pi / (phi + 1)) * (y + t) * stretch) * \
gfun((2 / (phi + 1)) * (stretch * (y + t) + (phi + 1)))
return xb + a1 + 1.25 * a2 + 1.25 ** 2 * a3
curve = [(x_from_y(float(y)), float(y)) for y in YS]
poly(curve + [(x_left, ymax), (x_left, ymin)], YELLOW)
- black outline around the leftmost yellow wave
g.add(dwg.polyline(
curve, fill="none", stroke=CHARCOAL, stroke_width=t, stroke_linecap="round", stroke_linejoin="round",
))
- Left labels (shift left a bit more so they never touch the man)
widths = np.array([x_from_y(float(y)) - x_left for y in YS]) idx = np.where((widths[1:-1] > widths[:-2]) & (widths[1:-1] > widths[2:]))[0] + 1 idx = idx[widths[idx] > 1e-6] idx = idx[np.argsort(widths[idx])[::-1]]
picked = [] min_sep = 0.35 for i in idx:
y = float(YS[i])
if all(abs(y - float(YS[j])) > min_sep for j in picked):
picked.append(i)
if len(picked) == 3:
break
picked = sorted(picked, key=lambda i: float(YS[i]), reverse=True)
- ----------------------------
- Right blocks (formerly BLUE -> STRIPED; RED/BLACK stay)
- ----------------------------
- Vertical stripes under the leftmost red bar block
rect(0.25 + 4 * (W / 5) + t / 2, 0.25 + W, ymin, ymax, STRIPED) rect(0.25 + 3 * (W / 5) + t / 2, 0.25 + 4 * (W / 5) - t / 2, -phi + t / 2, ymax, CHARCOAL) rect(0.25 + 2 * (W / 5) + t / 2, 0.25 + 3 * (W / 5) - t / 2, -W + t / 2, ymax, RED) rect(0.25 + 0 * (W / 5) + t / 2, 0.25 + 3 * (W / 5) - t / 2, -0.5 + t / 2, ymax, RED) rect(0.25 + 1 * (W / 5) + t / 2, 0.25 + 2 * (W / 5) - t / 2, -W + t / 2, -0.5 - t / 2, CHARCOAL) rect(0.25 + 0 * (W / 5) + t / 2, 0.25 + 1 * (W / 5) - t / 2, -W + t / 2, -0.5 - t / 2, RED) rect(0.25 + 1 * (W / 5) + t / 2, 0.25 + 3 * (W / 5) - t / 2, -phi + t / 2, -W - t / 2, RED) rect(0.25 + 1 * (W / 5) + t / 2, 0.25 + 4 * (W / 5) - t / 2, ymin, -phi - t / 2, RED)
x_bar_L = 0.25 + 0 * (W / 5) + t / 2 x_bar_R = 0.25 + 1 * (W / 5) - t / 2 rect(x_bar_L, x_bar_R, ymin, -W - t / 2, VSTRIPED)
- ------------------------------------------------------------
- Vertical title text in yellow (DER MODULOR)
- Place inside the large red region on the right
- ------------------------------------------------------------
x_text = 0.25 + 2.68 * (W / 5) y_text = 0.5 * (ymin + ymax) - 0.05 add_title_modulor(x_text, y_text, "LE MODULOR", angle_deg=-90, size=0.5)
- ----------------------------
- Envelopes
- - RHS envelopes include WHITE gap to x=t/2
- - LHS envelopes include SALMON strip from red boundary to envelope
- ----------------------------
- --- Envelopes strokes on top (NO fill here) ---
add_pair_envelopes_rhs_only(stroke=CHARCOAL, stroke_width=t, fill_gap=False, draw_stroke=True) draw_first_envelope_remove_first_wave(stroke=CHARCOAL, stroke_width=t, fill_gap=False, draw_stroke=True)
- NEW: ensure the leftmost envelope continues all the way to the top (y=ymax)
draw_left_top_envelope_and_salmon(
stroke=CHARCOAL, stroke_width=t, fill_salmon=True, fill_vstripes=True, draw_stroke=True
)
- existing lower envelopes
add_left_sliding_envelopes(stroke=CHARCOAL, stroke_width=t)
- ----------------------------
- Modulor
- ----------------------------
MODULOR_SVG_PATH = "ModulorDXF (2).svg" x_touch = -0.25 - t / 2 """add_modulor_outline(
MODULOR_SVG_PATH, x_touch=x_touch, x_panel_min=x_left, y_panel_min=ymin, y_panel_max=ymax, stroke=CHARCOAL, stroke_width=t, include_holes=True, fill_interior=True, fill_color=CHARCOAL,
)"""
- ----------------------------
- Frame & dividers (draw late so they sit on top)
- ----------------------------
rect(x_left - T, x_right + T, ymin - T, ymin, CHARCOAL) rect(x_left - T, x_right + T, ymax, ymax + T, CHARCOAL) rect(x_left - T, x_left, ymin - T, ymax + T, CHARCOAL) rect(x_right, x_right + T, ymin - T, ymax + T, CHARCOAL)
rect(-t / 2, t / 2, ymin, ymax, CHARCOAL) rect(-0.25 - t / 2, -0.25 + t / 2, ymin, ymax, CHARCOAL) rect(0.25 - t / 2, 0.25 + t / 2, ymin, ymax, CHARCOAL)
rect(0.25 + t / 2, 0.25 + 2 * (W / 5) + t / 2, -0.5 - t / 2, -0.5 + t / 2, CHARCOAL) rect(x_left, -0.25 - t / 2, -0.5 - t / 2, -0.5 + t / 2, CHARCOAL) rect(t / 2, 0.25 + 3 * (W / 5) - t / 2, -W - t / 2, -W + t / 2, CHARCOAL) rect(x_left, -0.25 - t / 2, -W - t / 2, -W + t / 2, CHARCOAL) rect(0.25 + t / 2, 0.25 + 4 * (W / 5) - t / 2, -phi - t / 2, -phi + t / 2, CHARCOAL)
x14, x13, x12, x11 = 0.25 + W / 5, 0.25 + 2 * W / 5, 0.25 + 3 * W / 5, 0.25 + 4 * W / 5 rect(x14 - t / 2, x14 + t / 2, ymin, -0.5 - t / 2, CHARCOAL) rect(x13 - t / 2, x13 + t / 2, -W + t / 2, -0.5 - t / 2, CHARCOAL) rect(x12 - t / 2, x12 + t / 2, -phi + t / 2, ymax, CHARCOAL) rect(x11 - t / 2, x11 + t / 2, ymin, ymax, CHARCOAL)
- Bars + labels on top
draw_bars_and_labels()
- ----------------------------
- LE CORBUSIER + DVA label boxes (draw late, on top)
- ----------------------------
- left panel bounds
x_left_panel_L = x_left x_left_panel_R = -0.25 - t / 2
- Place them visually like the reference:
x_box = x_left_panel_L + 0.56 * (x_left_panel_R - x_left_panel_L)
- LE CORBUSIER
add_box_label(
x_center=x_box, y_center=-0.92, text="LE CORBUSIER", w=0.6, h=0.118, stroke_width=0.009, font_weight="800", y_stretch=1.06, letter_spacing_em=-0.02, width_k=0.50, pad_frac=0.06,
)
- Save SVG
dwg.save() print("✅ Saved inequalities.svg")
import subprocess
import subprocess
subprocess.run([
"resvg", "inequalities.svg", "inequalities.png", "--width", "6000", "--height", "6000",
], check=True)
- ============================================================
- ALSO SAVE AS PNG (full SVG -> raster)
- Requires: pip install cairosvg
- ============================================================
Licensing
- You are free:
- to share – to copy, distribute and transmit the work
- to remix – to adapt the work
- Under the following conditions:
- attribution – You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
- share alike – If you remix, transform, or build upon the material, you must distribute your contributions under the same or compatible license as the original.
Captions
Items portrayed in this file
depicts
9 April 2026
1,877,362 byte
1,562 pixel
1,550 pixel
image/png
1404c0cd3d4ddb23ca82b622a0a837ba9a018457
File history
Click on a date/time to view the file as it appeared at that time.
| Date/Time | Thumbnail | Dimensions | User | Comment | |
|---|---|---|---|---|---|
| current | 12:03, 9 April 2026 | 1,550 × 1,562 (1.79 MB) | Xeno4n | Uploaded own work with UploadWizard |
File usage
The following page uses this file:
Metadata
This file contains additional information, probably added from the digital camera or scanner used to create or digitize it.
If the file has been modified from its original state, some details may not fully reflect the modified file.
| User comments | Screenshot |
|---|---|
| Horizontal resolution | 144 dpi |
| Vertical resolution | 144 dpi |