Refactor site to new theme
Some checks failed
Build and Deploy Hugo / deploy (push) Failing after 20s

This commit is contained in:
Alex Selimov 2026-03-02 14:56:56 -05:00
parent 1c5cdb3b97
commit 2519b96a86
16 changed files with 2590 additions and 83 deletions

View file

@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""Generate a triangulated mesh graphic as an SVG file.
Example:
python scripts/generate_triangulated_mesh.py --width 1920 --height 1080 --output mesh.svg
"""
from __future__ import annotations
import argparse
import math
import random
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Point:
x: float
y: float
def clamp(value: float, low: float, high: float) -> float:
return max(low, min(high, value))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate a triangulated mesh SVG.")
parser.add_argument("--width", type=int, required=True, help="Graphic width in pixels.")
parser.add_argument("--height", type=int, required=True, help="Graphic height in pixels.")
parser.add_argument(
"--output",
type=Path,
default=Path("triangulated-mesh.svg"),
help="Output SVG path (default: triangulated-mesh.svg)",
)
parser.add_argument(
"--cell-size",
type=float,
default=120.0,
help="Approximate grid cell size in pixels when x/y step are not set.",
)
parser.add_argument(
"--x-step",
type=float,
default=None,
help="Horizontal point spacing in pixels (overrides --cell-size on x-axis).",
)
parser.add_argument(
"--y-step",
type=float,
default=None,
help="Vertical point spacing in pixels (overrides --cell-size on y-axis).",
)
parser.add_argument(
"--jitter",
type=float,
default=0.35,
help="Point jitter amount as a fraction of cell size (0.0-0.5).",
)
parser.add_argument(
"--seed",
type=int,
default=None,
help="Random seed for reproducible output.",
)
parser.add_argument(
"--stroke",
type=float,
default=1.25,
help="Edge stroke width in pixels.",
)
parser.add_argument(
"--dot-radius",
type=float,
default=1.9,
help="Vertex dot radius in pixels.",
)
parser.add_argument(
"--line-color",
type=str,
default="#334155",
help="Edge color as hex (default: #334155).",
)
parser.add_argument(
"--dot-color",
type=str,
default="#0f172a",
help="Dot color as hex (default: #0f172a).",
)
parser.add_argument(
"--background",
type=str,
default="#f8fafc",
help="Background color as hex (default: #f8fafc).",
)
return parser.parse_args()
def build_points(
width: int,
height: int,
x_step: float,
y_step: float,
jitter_frac: float,
rng: random.Random,
) -> list[list[Point]]:
cols = max(2, math.ceil(width / x_step) + 1)
rows = max(2, math.ceil(height / y_step) + 1)
points: list[list[Point]] = []
jitter = clamp(jitter_frac, 0.0, 0.5) * min(x_step, y_step)
for row in range(rows):
y = (height * row) / (rows - 1)
row_points: list[Point] = []
for col in range(cols):
x = (width * col) / (cols - 1)
# Keep border anchored to make the mesh fill the canvas cleanly.
if 0 < row < rows - 1 and 0 < col < cols - 1:
x += rng.uniform(-jitter, jitter)
y_jittered = y + rng.uniform(-jitter, jitter)
else:
y_jittered = y
row_points.append(Point(x=clamp(x, 0.0, float(width)), y=clamp(y_jittered, 0.0, float(height))))
points.append(row_points)
return points
def generate_triangles(points: list[list[Point]], rng: random.Random) -> list[tuple[Point, Point, Point]]:
triangles: list[tuple[Point, Point, Point]] = []
rows = len(points)
cols = len(points[0]) if rows else 0
for row in range(rows - 1):
for col in range(cols - 1):
p00 = points[row][col]
p10 = points[row][col + 1]
p01 = points[row + 1][col]
p11 = points[row + 1][col + 1]
if rng.random() < 0.5:
triangles.append((p00, p10, p11))
triangles.append((p00, p11, p01))
else:
triangles.append((p00, p10, p01))
triangles.append((p10, p11, p01))
return triangles
def svg_polygon(points: tuple[Point, Point, Point], stroke: str, stroke_width: float) -> str:
pts = " ".join(f"{p.x:.2f},{p.y:.2f}" for p in points)
return (
f'<polygon points="{pts}" fill="none" '
f'stroke="{stroke}" stroke-width="{stroke_width:.3f}" stroke-linejoin="round" />'
)
def write_svg(
width: int,
height: int,
triangles: list[tuple[Point, Point, Point]],
stroke_width: float,
dot_radius: float,
line_color: str,
dot_color: str,
background: str,
output: Path,
points: list[list[Point]],
) -> None:
elements: list[str] = []
for tri in triangles:
elements.append(svg_polygon(tri, stroke=line_color, stroke_width=stroke_width))
circles: list[str] = []
for row in points:
for p in row:
circles.append(f'<circle cx="{p.x:.2f}" cy="{p.y:.2f}" r="{dot_radius:.2f}" fill="{dot_color}" />')
svg = "\n".join(
[
'<?xml version="1.0" encoding="UTF-8"?>',
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
f'<rect x="0" y="0" width="100%" height="100%" fill="{background}" />',
*elements,
*circles,
"</svg>",
]
)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(svg, encoding="utf-8")
def main() -> None:
args = parse_args()
if args.width <= 0 or args.height <= 0:
raise SystemExit("--width and --height must be positive integers")
if args.cell_size <= 1:
raise SystemExit("--cell-size must be > 1")
x_step = args.x_step if args.x_step is not None else args.cell_size
y_step = args.y_step if args.y_step is not None else args.cell_size
if x_step <= 1 or y_step <= 1:
raise SystemExit("--x-step and --y-step must be > 1")
rng = random.Random(args.seed)
points = build_points(args.width, args.height, x_step, y_step, args.jitter, rng)
triangles = generate_triangles(points, rng)
write_svg(
args.width,
args.height,
triangles,
args.stroke,
args.dot_radius,
args.line_color,
args.dot_color,
args.background,
args.output,
points,
)
print(f"Generated {len(triangles)} triangles -> {args.output}")
if __name__ == "__main__":
main()