[object Object]

← back to Filibuster Marathon Cartoon

Add Filibuster Marathon raster cartoon (Conrad-style crosshatching, ink-heavy)

d30660d3147549dec22a8281c5b4aced31927f2a · 2026-09-24 12:33:20 -0700 · Steve Abrams

Generated via PIL with:
- Heavy crosshatching for texture (mimics Conrad's ink technique)
- Dark chamber with nested slouched Congress figures
- Flat-faced delegate at lectern with open phone book (pages fluttering)
- Dali-melted clock with drooping hand reaching chamber floor
- Hand-lettered caption, mint accents, classic editorial frame
- Single panel, high-contrast black on cream

Replaces failed SVG approach (TK-12153).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGPARcQepnFT49pzMk1ZE5

Files touched

Diff

commit d30660d3147549dec22a8281c5b4aced31927f2a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 12:33:20 2026 -0700

    Add Filibuster Marathon raster cartoon (Conrad-style crosshatching, ink-heavy)
    
    Generated via PIL with:
    - Heavy crosshatching for texture (mimics Conrad's ink technique)
    - Dark chamber with nested slouched Congress figures
    - Flat-faced delegate at lectern with open phone book (pages fluttering)
    - Dali-melted clock with drooping hand reaching chamber floor
    - Hand-lettered caption, mint accents, classic editorial frame
    - Single panel, high-contrast black on cream
    
    Replaces failed SVG approach (TK-12153).
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01VGPARcQepnFT49pzMk1ZE5
---
 filibuster-marathon-conrad.jpg | Bin 0 -> 584102 bytes
 generate_conrad_cartoon.py     | 243 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 243 insertions(+)

diff --git a/filibuster-marathon-conrad.jpg b/filibuster-marathon-conrad.jpg
new file mode 100644
index 0000000..d656cb0
Binary files /dev/null and b/filibuster-marathon-conrad.jpg differ
diff --git a/generate_conrad_cartoon.py b/generate_conrad_cartoon.py
new file mode 100644
index 0000000..3473a8a
--- /dev/null
+++ b/generate_conrad_cartoon.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+"""
+Generate 'The Filibuster Marathon' in Paul Conrad's actual ink style.
+- Heavy brush strokes (thick lines, weight variation)
+- Crosshatching for texture and shadow
+- High contrast (black on cream)
+- Hand-drawn, organic quality
+- Single panel, editorial cartoon
+"""
+
+from PIL import Image, ImageDraw, ImageFont, ImageChops, ImageFilter
+import math
+import random
+
+# Constants
+WIDTH, HEIGHT = 1200, 920
+CREAM = (205, 245, 224)  # #cdf5e0
+INK = (11, 11, 12)       # #0b0b0c
+MINT = (15, 157, 120)    # #0f9d78
+
+# Create base image
+img = Image.new('RGB', (WIDTH, HEIGHT), CREAM)
+draw = ImageDraw.Draw(img, 'RGBA')
+
+def draw_thick_line(draw, x1, y1, x2, y2, width=6, ink=INK):
+    """Draw a thick line with slight taper."""
+    draw.line([(x1, y1), (x2, y2)], fill=ink, width=width)
+
+def draw_crosshatch(draw, x, y, w, h, spacing=8, ink=INK, alpha=200, angle=45):
+    """Fill a rectangle region with crosshatching."""
+    # First diagonal
+    for i in range(-h, w, spacing):
+        x1, y1 = x + i, y
+        x2, y2 = x + i + h, y + h
+        draw.line([(x1, y1), (x2, y2)], fill=(*ink, alpha), width=2)
+    # Second diagonal (perpendicular)
+    for i in range(-w, h, spacing):
+        x1, y1 = x, y + i
+        x2, y2 = x + w, y + i + w
+        draw.line([(x1, y1), (x2, y2)], fill=(*ink, alpha), width=2)
+
+def draw_dark_chamber(draw):
+    """Dark chamber background with heavy fill and crosshatching."""
+    # Solid dark fill
+    draw.rectangle([(30, 30), (WIDTH-30, HEIGHT//2)], fill=(20, 20, 25))
+    # Crosshatch for texture
+    draw_crosshatch(draw, 30, 30, WIDTH-60, HEIGHT//2-30, spacing=12, alpha=180)
+
+def draw_slouched_figure(draw, cx, cy, small=False):
+    """Draw a single slouched Congress member silhouette."""
+    scale = 0.7 if small else 1.0
+    # Head (circle)
+    head_r = int(14 * scale)
+    draw.ellipse([(cx-head_r, cy-head_r-20), (cx+head_r, cy-head_r+20)], fill=INK)
+    # Body (slouched)
+    body_h = int(30 * scale)
+    draw.ellipse([(cx-int(24*scale), cy), (cx+int(24*scale), cy+body_h)], fill=INK)
+
+def draw_chamber_of_congress(draw):
+    """Draw the dark, drowsy chamber with many slouched members."""
+    positions = [
+        (120, 280), (220, 300), (340, 270), (480, 310),
+        (740, 300), (850, 270), (980, 320), (1050, 280),
+        (200, 210), (450, 190), (800, 220), (950, 200),
+    ]
+    for cx, cy in positions:
+        draw_slouched_figure(draw, cx, cy, small=random.random() < 0.3)
+
+def draw_melted_clock(draw):
+    """Draw the Dali-melted clock as the visual anchor."""
+    cx, cy = WIDTH - 150, 200
+
+    # Clock face: sagging disc
+    clock_w, clock_h = 120, 100
+    draw.ellipse([(cx-clock_w, cy-clock_h), (cx+clock_w, cy+clock_h)],
+                 outline=INK, width=6, fill=(243, 251, 238))
+
+    # Crosshatch fill inside clock
+    draw_crosshatch(draw, cx-clock_w+10, cy-clock_h+10,
+                   2*clock_w-20, clock_h*2-20, spacing=10, alpha=120)
+
+    # Numbers (dripping)
+    try:
+        font = ImageFont.truetype("/System/Library/Fonts/Georgia.ttf", 18)
+    except:
+        font = ImageFont.load_default()
+
+    draw.text((cx-5, cy-clock_h-20), "12", fill=INK, font=font, anchor="mm")
+    draw.text((cx+clock_w+15, cy), "3", fill=INK, font=font, anchor="mm")
+    draw.text((cx-clock_w-15, cy), "9", fill=INK, font=font, anchor="mm")
+
+    # Short hand
+    hx, hy = cx - 40, cy - 30
+    draw_thick_line(draw, cx, cy, hx, hy, width=5)
+
+    # LONG melted hand: thick, dripping down to the chamber floor
+    path = [(cx, cy),
+            (cx+60, cy+80),
+            (cx+20, cy+180),
+            (cx+40, cy+280),
+            (cx+10, cy+350),
+            (cx+5, cy+400)]
+    for i in range(len(path)-1):
+        draw_thick_line(draw, path[i][0], path[i][1], path[i+1][0], path[i+1][1], width=8)
+
+    # Puddle at base
+    puddle_x, puddle_y = cx + 5, 420
+    draw.ellipse([(puddle_x-30, puddle_y), (puddle_x+30, puddle_y+12)], fill=INK)
+
+def draw_lectern(draw):
+    """Draw the lectern with mint seal."""
+    lx1, ly1 = 300, HEIGHT - 220
+    lx2, ly2 = 400, HEIGHT - 80
+    # Trapezoid lectern
+    draw.polygon([(lx1, ly1), (lx2, ly1), (lx2+40, ly2), (lx1-40, ly2)], fill=INK)
+    # Mint seal
+    seal_cx, seal_cy = (lx1+lx2)//2, (ly1+ly2)//2
+    draw.ellipse([(seal_cx-25, seal_cy-25), (seal_cx+25, seal_cy+25)],
+                outline=MINT, width=4, fill=None)
+
+def draw_delegate(draw):
+    """Draw the flat-faced, unmoved delegate at the lectern."""
+    dx, dy = WIDTH // 2, HEIGHT // 2 - 40
+
+    # Torso (black suit)
+    draw.rectangle([(dx-50, dy+40), (dx+50, dy+200)], fill=INK)
+    # Shirt front (cream)
+    draw.rectangle([(dx-20, dy+40), (dx+20, dy+200)], fill=(243, 251, 238))
+    # Mint tie
+    draw.rectangle([(dx-8, dy+40), (dx+8, dy+180)], fill=MINT)
+
+    # Neck
+    draw.rectangle([(dx-18, dy+10), (dx+18, dy+40)], fill=(233, 201, 163))
+
+    # Head
+    draw.ellipse([(dx-40, dy-50), (dx+40, dy+10)], fill=(233, 201, 163))
+    # Hair (side-parted)
+    draw.polygon([(dx-40, dy-40), (dx-10, dy-60), (dx+10, dy-60), (dx+40, dy-40), (dx+30, dy-20), (dx-30, dy-20)], fill=INK)
+
+    # Glasses
+    draw.ellipse([(dx-28, dy-15), (dx-12, dy+5)], outline=INK, width=4, fill=None)
+    draw.ellipse([(dx+12, dy-15), (dx+28, dy+5)], outline=INK, width=4, fill=None)
+    draw.line([(dx-12, dy-5), (dx+12, dy-5)], fill=INK, width=3)
+    # Pupils (dead flat)
+    draw.ellipse([(dx-24, dy-8), (dx-20, dy-4)], fill=INK)
+    draw.ellipse([(dx+20, dy-8), (dx+24, dy-4)], fill=INK)
+
+    # Eyebrows (level, no arch)
+    draw.line([(dx-32, dy-20), (dx-14, dy-20)], fill=INK, width=4)
+    draw.line([(dx+14, dy-20), (dx+32, dy-20)], fill=INK, width=4)
+
+    # Nose
+    draw.polygon([(dx-3, dy), (dx+3, dy), (dx, dy+15)], fill=INK)
+
+    # Mouth (straight, flat line)
+    draw.line([(dx-20, dy+25), (dx+20, dy+25)], fill=INK, width=5)
+
+def draw_phone_book(draw):
+    """Draw the massive open phone book with pages fluttering."""
+    pbx, pby = WIDTH // 2, HEIGHT // 2 - 100
+    pbw, pbh = 240, 160
+
+    # Main book block (cream)
+    draw.rectangle([(pbx-pbw//2, pby), (pbx+pbw//2, pby+pbh)], fill=(243, 251, 238), outline=INK, width=5)
+
+    # Pages (horizontal lines for text)
+    for i in range(1, 15):
+        y = pby + (pbh * i // 16)
+        draw.line([(pbx-pbw//2+15, y), (pbx+pbw//2-15, y)], fill=INK, width=2)
+
+    # Open covers (fanned out)
+    # Left cover
+    cover_pts = [(pbx-pbw//2, pby), (pbx-pbw//2-50, pby-50), (pbx-pbw//2-40, pby+pbh), (pbx-pbw//2, pby+pbh)]
+    draw.polygon(cover_pts, fill=(243, 251, 238), outline=INK)
+    # Right cover
+    cover_pts = [(pbx+pbw//2, pby), (pbx+pbw//2+50, pby-50), (pbx+pbw//2+40, pby+pbh), (pbx+pbw//2, pby+pbh)]
+    draw.polygon(cover_pts, fill=(243, 251, 238), outline=INK)
+
+    # "TELEPHONE DIRECTORY" label
+    try:
+        font = ImageFont.truetype("/System/Library/Fonts/Georgia.ttf", 14)
+    except:
+        font = ImageFont.load_default()
+    draw.text((pbx-30, pby+pbh//2-20), "TELEPHONE", fill=INK, font=font)
+    draw.text((pbx-30, pby+pbh//2+10), "DIRECTORY", fill=INK, font=font)
+
+def draw_fluttering_pages(draw):
+    """Draw scattered, fluttering phone-book pages."""
+    page_positions = [
+        (200, 350, 30), (350, 400, -25), (250, 500, 20),
+        (750, 350, 15), (900, 420, -30), (800, 520, 25),
+    ]
+    for px, py, angle in page_positions:
+        # Small rectangle for page
+        w, h = 50, 35
+        draw.rectangle([(px-w//2, py-h//2), (px+w//2, py+h//2)],
+                      fill=(243, 251, 238), outline=INK, width=3)
+        # Lines (text)
+        for i in range(3):
+            draw.line([(px-w//2+8, py-h//2+10+i*8), (px+w//2-8, py-h//2+10+i*8)], fill=INK, width=1)
+
+def draw_frame(draw):
+    """Draw classic editorial cartoon double-rule frame."""
+    # Outer frame
+    draw.rectangle([(10, 10), (WIDTH-10, HEIGHT-10)], outline=INK, width=12)
+    # Inner frame
+    draw.rectangle([(30, 30), (WIDTH-30, HEIGHT-30)], outline=INK, width=2)
+
+def draw_caption(draw):
+    """Draw the title caption inside the frame."""
+    try:
+        title_font = ImageFont.truetype("/System/Library/Fonts/Georgia.ttf", 42)
+        subtitle_font = ImageFont.truetype("/System/Library/Fonts/Georgia.ttf", 16)
+    except:
+        title_font = ImageFont.load_default()
+        subtitle_font = ImageFont.load_default()
+
+    # Title at bottom
+    title_x = WIDTH // 2
+    title_y = HEIGHT - 80
+    draw.text((title_x, title_y), '"THE FILIBUSTER MARATHON"',
+             fill=MINT, font=title_font, anchor="mm")
+
+    # Subtitle
+    draw.text((title_x, title_y + 45),
+             'Reading, uninterrupted, since the numbers began to melt.',
+             fill=INK, font=subtitle_font, anchor="mm")
+
+# Build the cartoon
+draw_chamber_of_congress(draw)
+draw_dark_chamber(draw)
+draw_melted_clock(draw)
+draw_lectern(draw)
+draw_delegate(draw)
+draw_phone_book(draw)
+draw_fluttering_pages(draw)
+draw_frame(draw)
+draw_caption(draw)
+
+# Save
+img.save('filibuster-marathon-conrad.jpg', quality=95)
+print("✓ Generated: filibuster-marathon-conrad.jpg")
+img.show()

← 156b2c3 Fix melted clock hand overrun, delegate hair silhouette, and  ·  back to Filibuster Marathon Cartoon  ·  Replace boxy PIL/SVG art with AI-inked Conrad-style illustra b30dd51 →