← back to Crazy News Channel Shadowman

cartoons/20260924-pork-barrel-train.html

359 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Pork Barrel Express — P24 Political Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: a fictional infrastructure bill turns into a runaway train as wildly unrelated riders couple on car by car. Click a car for its invented cost." />
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body data-mood="politics" class="cartoon-page cartoon-page-wide">
<header>
  <div>
    <div class="kicker">P24 Political Cartoon Desk &middot; Invented Satire</div>
    <h1>The Pork Barrel Express</h1>
    <p class="dek">An invented infrastructure bill leaves committee as a single train car and picks up wildly unrelated riders at every stop. Click a car to see what it actually costs.</p>
  </div>
  <div class="header-actions">
    <a class="back" href="index.html">&larr; Cartoon Desk</a>
  </div>
</header>
<main>
  <div class="strip" id="strip">
    <figure class="panel" data-i="0">
      <div class="phead"><span>Panel 1 &middot; Introduced</span><span id="stamp0"></span></div>
      <svg class="art" id="art0" viewBox="0 0 300 300" role="img" tabindex="0"></svg>
      <button class="cover" id="cover0" type="button">Click, tap, or press N to begin</button>
      <figcaption id="cap0"></figcaption>
    </figure>
    <figure class="panel" data-i="1">
      <div class="phead"><span>Panel 2 &middot; Riders Couple On</span><span id="stamp1"></span></div>
      <svg class="art" id="art1" viewBox="0 0 300 300" role="img" tabindex="0"></svg>
      <button class="cover" id="cover1" type="button" disabled>In queue</button>
      <figcaption id="cap1"></figcaption>
    </figure>
    <figure class="panel" data-i="2">
      <div class="phead"><span>Panel 3 &middot; Fully Loaded</span><span id="stamp2"></span></div>
      <svg class="art" id="art2" viewBox="0 0 300 300" role="img" tabindex="0"></svg>
      <button class="cover" id="cover2" type="button" disabled>In queue</button>
      <figcaption id="cap2"></figcaption>
    </figure>
  </div>

  <div class="dock">
    <button class="primary" id="nextBtn" type="button">Process Panel 1</button>
    <button class="twist" id="twistBtn" type="button">Plot Twist!</button>
    <div id="status" role="status" aria-live="polite"></div>
  </div>
  <p class="hint">Keys: N/&rarr; next panel &middot; T plot twist &middot; click a panel to advance &middot; click a car for its invented cost</p>
</main>
<footer>H.B. 4417, the Assembly of Larkspur, every official, town, and train car pictured here are invented. No actual legislature, lighthouse, or ferris wheel was ever billed to a taxpayer. Part of PANDEMONIUM-24.</footer>

<script>
(function(){
  const NS = 'http://www.w3.org/2000/svg';
  function el(tag, attrs, parent){
    const n = document.createElementNS(NS, tag);
    for (const k in attrs) n.setAttribute(k, attrs[k]);
    if (parent) parent.appendChild(n);
    return n;
  }
  function reduced(){ return matchMedia('(prefers-reduced-motion: reduce)').matches; }

  // ---- invented "official" mascots — simple flat iconographic seals, not detailed characters ----
  const MASCOTS = [
    { name: 'Comptroller Thistlewood', color: '#b5302f' },
    { name: 'Assemblymember Grigg', color: '#0f6b52' },
    { name: 'Track Overseer Pruitt', color: '#c9a227' },
    { name: 'Whip Delacroix', color: '#6b4fa0' },
  ];
  let mascotIdx = 0;

  const BILL_NAME = 'H.B. 4417 — The Interstate Rail Modernization Act';
  const LEGISLATURE = 'the Assembly of Larkspur';

  // ---- invented cars: the real bill car, riders that couple on by panel, and a rotating pool for twists ----
  const BASE_CAR   = { name: 'The Interstate Rail Modernization Car (the actual bill)', short: 'RAIL CORE', cost: 40000000 };
  const RIDERS_2 = [
    { name: 'Desert Lighthouse Car', short: 'LIGHTHOUSE', cost: 12000000 },
    { name: 'Ferris Wheel Pavilion Car', short: 'FERRIS WHEEL', cost: 8500000 },
  ];
  const RIDERS_3 = [
    { name: 'Commemorative Spoon Museum Car', short: 'SPOON MUSEUM', cost: 3200000 },
    { name: 'Heated Sidewalk (Zero-Snowfall Town) Car', short: 'HOT SIDEWALK', cost: 6750000 },
    { name: 'Bridge to the Island (Location TBD) Car', short: 'BRIDGE TBD', cost: 95000000 },
  ];
  const TWIST_POOL = [
    { name: 'Underwater Petting Zoo Car', short: 'PETTING ZOO', cost: 22000000 },
    { name: 'Solar-Powered Sundial Car', short: 'SUNDIAL', cost: 1400000 },
    { name: 'Rolling Cheese Wheel Track Car', short: 'CHEESE TRACK', cost: 4800000 },
    { name: 'Indoor Snowless Ski Slope Car', short: 'SKI SLOPE', cost: 61000000 },
    { name: 'Museum of Robotic Roosters Car', short: 'ROBO ROOSTERS', cost: 5600000 },
    { name: 'Scenic Overlook With No View Car', short: 'NO-VIEW LOOKOUT', cost: 900000 },
    { name: 'Two-Story Garage for One Car Car', short: '1-CAR GARAGE', cost: 3300000 },
  ];

  const state = { revealed: 0, twists: 0 };
  const costState = {}; // key `${panelIndex}:${carIdx}` -> boolean, whether the cost callout is open
  let focusAfterRender = null; // {panel, idx}

  function twistCars(){
    const arr = [];
    for (let t = 0; t < state.twists; t++) arr.push(TWIST_POOL[t % TWIST_POOL.length]);
    return arr;
  }

  function carsForPanel(i){
    if (i === 0) return [BASE_CAR];
    if (i === 1) return [BASE_CAR, ...RIDERS_2];
    return [BASE_CAR, ...RIDERS_2, ...RIDERS_3, ...twistCars()];
  }

  function fmtMoney(n){ return '$' + n.toLocaleString('en-US'); }
  function fmtMoneyShort(n){ return '$' + Math.round(n / 1e6) + 'M'; }
  function totalCost(cars){ return cars.reduce((s, c) => s + c.cost, 0); }

  function exasperation(n){
    if (n <= 3) return 'a modest bill so far';
    if (n <= 5) return 'already sprawling';
    if (n <= 8) return 'frankly ridiculous';
    if (n <= 11) return 'a fiscal cry for help';
    return 'no longer a train, more of a rolling apology';
  }

  function captionFor(i){
    const cars = carsForPanel(i);
    const total = totalCost(cars);
    if (i === 0) {
      return `${BILL_NAME} clears committee in ${LEGISLATURE} with exactly one car: the actual train. Running total: ${fmtMoney(total)}.`;
    }
    if (i === 1) {
      const riders = cars.length - 1;
      return `Before the ink is dry, ${riders} unrelated rider${riders === 1 ? '' : 's'} couple${riders === 1 ? 's' : ''} on. The train now hauls ${cars.length} cars — running total ${fmtMoney(total)}.`;
    }
    const twistNote = state.twists > 0 ? ` Plot twist #${state.twists} just tacked on another car.` : '';
    return `${cars.length} cars now, running total ${fmtMoney(total)} — ${exasperation(cars.length)}.${twistNote}`;
  }

  // ---- shared flat iconographic drawing helpers ----
  function official(svg, cx, cy, r, color, label){
    const g = el('g', {}, svg);
    el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.15', stroke: color, 'stroke-width': 3 }, g);
    el('circle', { cx, cy: cy - r*0.35, r: r*0.28, fill: color }, g); // head
    el('path', { d: `M ${cx - r*0.5} ${cy + r*0.5} Q ${cx} ${cy + r*0.1} ${cx + r*0.5} ${cy + r*0.5}`, fill: 'none', stroke: color, 'stroke-width': r*0.22, 'stroke-linecap':'round' }, g); // shoulders
    if (label) {
      const t = el('text', { x: cx, y: cy + r + 16, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'10', fill: 'currentColor' }, g);
      t.textContent = label;
    }
    return g;
  }

  function seal(svg, cx, cy, r, text, color){
    const g = el('g', { transform: `rotate(-12 ${cx} ${cy})` }, svg);
    el('circle', { cx, cy, r, fill: 'none', stroke: color, 'stroke-width': 4, 'stroke-dasharray':'4 3' }, g);
    const t = el('text', { x: cx, y: cy+4, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'800', 'font-size': Math.max(9, r*0.34), fill: color }, g);
    t.textContent = text;
    return g;
  }

  function track(svg){
    el('line', { x1:10, y1:230, x2:290, y2:230, stroke:'currentColor', 'stroke-width':3, opacity:0.55 }, svg);
    for (let x = 14; x <= 286; x += 14) {
      el('line', { x1:x, y1:227, x2:x-4, y2:236, stroke:'currentColor', 'stroke-width':2, opacity:0.35 }, svg);
    }
  }

  function drawEngine(svg, x, y, w, h, color){
    const g = el('g', {}, svg);
    el('rect', { x, y, width:w, height:h, rx:6, fill:color, 'fill-opacity':0.22, stroke:color, 'stroke-width':3 }, g);
    el('rect', { x:x+w*0.55, y:y-14, width:w*0.4, height:14, fill:color, 'fill-opacity':0.32, stroke:color, 'stroke-width':2 }, g);
    el('rect', { x:x+w*0.14, y:y-11, width:6, height:11, fill:color }, g);
    el('circle', { cx:x+w*0.28, cy:y+h+6, r:6.5, fill:'currentColor', opacity:0.6 }, g);
    el('circle', { cx:x+w*0.72, cy:y+h+6, r:6.5, fill:'currentColor', opacity:0.6 }, g);
  }

  function wrapShort(text, group, cx, y, size){
    const words = text.split(' ');
    let line1 = '', line2 = '';
    let i = 0;
    while (i < words.length && (line1 + words[i]).length <= 9) { line1 += (line1 ? ' ' : '') + words[i]; i++; }
    line2 = words.slice(i).join(' ');
    const t = el('text', { x: cx, y, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'700', 'font-size': size, fill:'currentColor' }, group);
    const ts1 = el('tspan', { x: cx, dy: 0 }, t);
    ts1.textContent = line1;
    if (line2) {
      const ts2 = el('tspan', { x: cx, dy: size + 1 }, t);
      ts2.textContent = line2;
    }
  }

  function drawCar(svg, x, y, w, h, car, idx, panelIndex, color, positions){
    const key = panelIndex + ':' + idx;
    const g = el('g', {
      class: 'car', tabindex: '0', role: 'button',
      'aria-label': `${car.name} — click to reveal its invented cost`,
      'aria-pressed': costState[key] ? 'true' : 'false',
      id: `car-${panelIndex}-${idx}`,
    }, svg);
    el('rect', { x, y, width:w, height:h, rx:5, fill:color, 'fill-opacity':0.15, stroke:color, 'stroke-width':2.4 }, g);
    el('circle', { cx:x+w*0.25, cy:y+h+6, r:6, fill:'currentColor', opacity:0.55 }, g);
    el('circle', { cx:x+w*0.75, cy:y+h+6, r:6, fill:'currentColor', opacity:0.55 }, g);
    // $ affordance badge, top-right corner of the car — signals "click me"
    el('circle', { cx:x+w-7, cy:y+7, r:6.5, fill: color }, g);
    const dt = el('text', { x:x+w-7, y:y+10, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'800', 'font-size':'8', fill:'var(--panel)' }, g);
    dt.textContent = '$';
    const fontSize = Math.max(5.5, Math.min(8, w*0.17));
    wrapShort(car.short, g, x + w/2, y + h*0.62, fontSize);
    g.addEventListener('click', (e) => { e.stopPropagation(); toggleCost(panelIndex, idx); });
    g.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); toggleCost(panelIndex, idx); }
    });
    positions.push({ x, y, w, h, car, idx });
  }

  function drawCallout(svg, pos, panelIndex){
    const boxW = 118, boxH = 34;
    const cx = pos.x + pos.w/2;
    let bx = cx - boxW/2;
    bx = Math.max(4, Math.min(300 - boxW - 4, bx));
    let by = pos.y - boxH - 8;
    if (by < 4) by = pos.y + pos.h + 16;
    const g = el('g', { class:'cost-callout', style:'pointer-events:none' }, svg);
    el('rect', { x:bx, y:by, width:boxW, height:boxH, rx:6, fill:'var(--panel)', stroke:'var(--rule)', 'stroke-width':2 }, g);
    const t1 = el('text', { x:bx+boxW/2, y:by+14, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'8.5', 'font-weight':'700', fill:'currentColor' }, g);
    t1.textContent = pos.car.short;
    const t2 = el('text', { x:bx+boxW/2, y:by+27, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'11', 'font-weight':'800', fill:'var(--accent)' }, g);
    t2.textContent = fmtMoney(pos.car.cost);
  }

  function toggleCost(panelIndex, idx){
    const key = panelIndex + ':' + idx;
    costState[key] = !costState[key];
    focusAfterRender = { panel: panelIndex, idx };
    renderPanel(panelIndex);
  }

  function drawTrain(svg, panelIndex, cars, mascot){
    svg.innerHTML = '';
    el('rect', { x:0, y:0, width:300, height:300, fill:'none' }, svg);
    track(svg);

    const t = el('text', { x:150, y:14, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'9.5', 'font-weight':'700', fill:'currentColor' }, svg);
    t.textContent = panelIndex === 0 ? BILL_NAME : `${BILL_NAME} + riders`;

    official(svg, 38, 122, 22, mascot.color, mascot.name);

    const total = totalCost(cars);
    seal(svg, 252, 66, 26, fmtMoneyShort(total), mascot.color);

    const trackLeft = 14, trackRight = 286;
    const engineW = 42, gap = 4, h = 46, y = 176;
    const n = cars.length;
    const avail = (trackRight - trackLeft) - engineW - gap;
    const carW = Math.max(18, Math.min(44, avail / n - gap));

    let x = trackLeft;
    drawEngine(svg, x, y, engineW, h, mascot.color);
    x += engineW + gap;

    const positions = [];
    cars.forEach((car, idx) => {
      const color = idx === 0 ? mascot.color : MASCOTS[(mascotIdx + idx) % MASCOTS.length].color;
      drawCar(svg, x, y, carW, h, car, idx, panelIndex, color, positions);
      x += carW + gap;
    });

    positions.forEach((pos) => {
      if (costState[panelIndex + ':' + pos.idx]) drawCallout(svg, pos, panelIndex);
    });

    if (focusAfterRender && focusAfterRender.panel === panelIndex) {
      const target = document.getElementById(`car-${panelIndex}-${focusAfterRender.idx}`);
      if (target) target.focus();
      focusAfterRender = null;
    }
  }

  const STAMPS = ['FILED', 'COUPLED', 'LOADED'];

  function renderPanel(i){
    const svg = document.getElementById('art' + i);
    const mascot = MASCOTS[(mascotIdx + i) % MASCOTS.length];
    const cars = carsForPanel(i);
    drawTrain(svg, i, cars, mascot);
    document.getElementById('cap' + i).textContent = captionFor(i);
    document.getElementById('stamp' + i).textContent = i < state.revealed ? STAMPS[i] : '';
    const cover = document.getElementById('cover' + i);
    cover.hidden = i <= state.revealed - 1 || (i === state.revealed);
    if (i === state.revealed) { cover.hidden = false; cover.disabled = false; cover.textContent = i === 0 ? 'Click, tap, or press N to begin' : 'Click, tap, or press N to process'; }
  }

  function paintAll(){
    for (let i=0;i<3;i++) {
      renderPanel(i);
      document.getElementById('art'+i).setAttribute('tabindex', i === state.revealed ? '0' : '-1');
    }
    syncButtons();
  }

  function syncButtons(){
    const nextBtn = document.getElementById('nextBtn');
    if (state.revealed >= 3) { nextBtn.textContent = 'All 3 panels processed ✓'; nextBtn.disabled = true; }
    else { nextBtn.textContent = `Process Panel ${state.revealed+1}`; nextBtn.disabled = false; }
  }

  function say(kind, msg){
    const s = document.getElementById('status');
    s.className = kind; s.textContent = msg;
  }

  function next(){
    if (state.revealed >= 3) { say('info', 'All 3 panels are already on file. Try Plot Twist!'); return; }
    state.revealed++;
    for (let i=0;i<3;i++) {
      const cover = document.getElementById('cover'+i);
      cover.hidden = i < state.revealed;
      cover.disabled = i > state.revealed;
      document.getElementById('art'+i).setAttribute('tabindex', i === state.revealed ? '0' : '-1');
    }
    renderPanel(state.revealed - 1); // the panel that just got processed
    if (state.revealed < 3) renderPanel(state.revealed); // the newly-active cover's label
    say('ok', `Panel ${state.revealed} processed.`);
    syncButtons();
    if (state.revealed >= 3) say('ok', 'All 3 panels processed. Press Plot Twist! to couple on another car.');
  }

  function twist(){
    if (state.revealed < 3) { say('err', 'ERROR RIDER-402 · No twist before the train is fully loaded — process all 3 panels first.'); return; }
    mascotIdx = (mascotIdx + 1) % MASCOTS.length;
    state.twists++;
    renderPanel(0); renderPanel(1); renderPanel(2);
    const cars = carsForPanel(2);
    say('ok', `Plot twist #${state.twists}: another rider couples onto the back of the train. ${cars.length} cars now, running total ${fmtMoney(totalCost(cars))}.`);
  }

  document.getElementById('nextBtn').addEventListener('click', next);
  document.getElementById('twistBtn').addEventListener('click', twist);
  for (let i=0;i<3;i++) {
    document.getElementById('cover'+i).addEventListener('click', () => { if (i === state.revealed) next(); });
    document.getElementById('art'+i).addEventListener('click', () => { if (i === state.revealed) next(); });
    document.getElementById('art'+i).addEventListener('keydown', (e) => { if ((e.key==='Enter'||e.key===' ') && i===state.revealed) { e.preventDefault(); next(); } });
  }


  document.addEventListener('keydown', (e) => {
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    const tag = (e.target.tagName || '').toLowerCase();
    if (tag === 'input' || tag === 'textarea') return;
    const k = e.key.toLowerCase();
    if (k === 'n' || e.key === 'ArrowRight') { e.preventDefault(); next(); }
    else if (k === 't') { e.preventDefault(); twist(); }
  });

  paintAll();
  say('info', 'Case file open. Press N or click Panel 1 to begin.');
})();
</script>
</body>
</html>