← back to Crazy News Channel Shadowman

cartoons/20260924-budget-pie-fight.html

341 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Who Gets the Last Slice — P24 Political Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: an invented town budget is a pie that diners keep re-slicing smaller at a banquet table. Drag the slider to re-carve it and watch the caption rewrite live." />
<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>Who Gets the Last Slice</h1>
    <p class="dek">An invented town's budget arrives at the banquet table as one pie. By dessert, five invented departments are still holding forks and the pie has not gotten any bigger.</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; Opening Slices</span><span id="stamp0"></span></div>
      <svg class="art" id="art0" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Round 1 pie chart"></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; The Re-Carving</span><span id="stamp1"></span></div>
      <svg class="art" id="art1" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Round 2 pie chart"></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; Crumbs Only</span><span id="stamp2"></span></div>
      <svg class="art" id="art2" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Round 3 pie chart"></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 class="slider-row">
      <div class="row1">
        <label for="pieSlider">Re-carve <strong>Municipal Ducts'</strong> slice</label>
        <span class="val" id="sliderVal">—</span>
      </div>
      <input type="range" id="pieSlider" min="0" max="100" value="0" step="1"
        aria-label="Redistribute the Municipal Ducts department's percentage of the active round's pie"
        aria-describedby="sliderVal" disabled />
    </div>
    <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; drag the slider to re-carve the active round's pie</p>
</main>
<footer>All towns, departments, officials, and mascots are invented. No actual municipal budget was harmed. 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;
  }

  // ---- invented departments (fictional, non-partisan) ----
  const DEPARTMENTS = [
    { name: 'Parks & Wonder', color: '#0f6b52' },
    { name: 'Municipal Ducts', color: '#b5302f' },
    { name: 'Bureau of Loud Noises', color: '#c9a227' },
    { name: 'Office of Overflow Parking', color: '#6b4fa0' },
    { name: 'Dept. of Suggestive Signage', color: '#2f6fb5' },
  ];
  const MOD_IDX = 1; // Municipal Ducts — the slider-controlled department

  const ROUNDS = [
    { reserved: 0, shape: [24,22,20,18,16], title: 'Round 1 — Opening Slices',
      blurb: 'The pie lands on the table whole. Every mascot gets an honest slice — for now.' },
    { reserved: 35, shape: [26,16,22,14,22], title: 'Round 2 — The Re-Carving',
      blurb: 'The table reserves over a third of the pie "for planning purposes." Nobody has seen the plan.' },
    { reserved: 62, shape: [30,10,24,10,26], title: 'Round 3 — Crumbs Only',
      blurb: 'The reserved zone now swallows most of the pie. What is left gets fought over in crumbs.' },
  ];

  const CAPTIONS = ROUNDS.map(r => r.blurb);

  const state = { revealed: 0, twists: 0, sliderOverride: [null, null, null], touched: [false, false, false] };

  function computeSlices(roundIdx, modPct, twistCount){
    const round = ROUNDS[roundIdx];
    const shape = round.shape;
    const othersShapeSum = shape.reduce((s,v,i) => i === MOD_IDX ? s : s + v, 0);
    const othersTarget = 100 - modPct;
    let slices = DEPARTMENTS.map((d,i) => {
      let shapeShare;
      if (i === MOD_IDX) shapeShare = modPct;
      else shapeShare = othersShapeSum > 0 ? (shape[i] / othersShapeSum) * othersTarget : 0;
      const pct = shapeShare / 100 * (100 - round.reserved);
      return { name: d.name, color: d.color, pct, dept: true };
    });
    if (round.reserved > 0) {
      slices.push({ name: 'Reserved (Off-Limits)', color: '#9a9086', pct: round.reserved, reserved: true });
    }
    if (twistCount > 0) {
      const adminFee = Math.min(45, 10 + 8 * twistCount);
      const scale = (100 - adminFee) / 100;
      slices = slices.map(s => ({ ...s, pct: s.pct * scale }));
      slices.push({ name: 'Administrative Fee', color: '#5a5248', pct: adminFee, admin: true });
    }
    return slices;
  }

  function currentModPct(i){
    return state.sliderOverride[i] != null ? state.sliderOverride[i] : ROUNDS[i].shape[MOD_IDX];
  }

  function polar(cx, cy, r, angleDeg){
    const rad = angleDeg * Math.PI / 180;
    return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
  }
  function arcPath(cx, cy, r, startDeg, endDeg){
    const start = polar(cx, cy, r, startDeg);
    const end = polar(cx, cy, r, endDeg);
    const largeArc = (endDeg - startDeg) > 180 ? 1 : 0;
    return `M ${cx} ${cy} L ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${r} ${r} 0 ${largeArc} 1 ${end.x.toFixed(2)} ${end.y.toFixed(2)} Z`;
  }

  function mascot(svg, cx, cy, r, color, label){
    const g = el('g', {}, svg);
    el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.18', stroke: color, 'stroke-width': 2.5 }, g);
    el('circle', { cx, cy: cy - r*0.35, r: r*0.3, fill: color }, g); // head
    el('path', { d: `M ${cx - r*0.5} ${cy + r*0.5} Q ${cx} ${cy + r*0.15} ${cx + r*0.5} ${cy + r*0.5}`, fill: 'none', stroke: color, 'stroke-width': r*0.24, 'stroke-linecap':'round' }, g); // shoulders
    if (label) {
      const t = el('text', { x: cx, y: cy + r + 13, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'8.5', fill: 'currentColor' }, g);
      const words = label.split(' ');
      const lines = [];
      let line = '';
      words.forEach(w => {
        if ((line + ' ' + w).trim().length > 14) { lines.push(line.trim()); line = w; }
        else line = (line + ' ' + w).trim();
      });
      if (line) lines.push(line);
      lines.forEach((ln, idx) => {
        const ts = el('tspan', { x: cx, dy: idx === 0 ? 0 : 10 }, t);
        ts.textContent = ln;
      });
    }
    return g;
  }

  function drawScene(svg, roundIdx, modPct, twistCount){
    svg.innerHTML = '';
    const cx = 150, cy = 150, r = 62;
    const round = ROUNDS[roundIdx];
    const slices = computeSlices(roundIdx, modPct, twistCount);

    const title = el('text', { x:150, y:16, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'700', 'font-size':'10', fill:'currentColor' }, svg);
    title.textContent = round.title.toUpperCase();

    el('rect', { x:40, y:238, width:220, height:8, fill:'currentColor', opacity:0.35, rx:2 }, svg); // banquet table edge

    let angle = -90;
    slices.forEach(s => {
      const sweep = (s.pct / 100) * 360;
      if (sweep > 0.2) {
        const d = arcPath(cx, cy, r, angle, angle + sweep);
        el('path', { d, fill: s.color, stroke: 'var(--panel)', 'stroke-width': 2,
          'stroke-dasharray': s.admin ? '3 2' : (s.reserved ? '5 3' : 'none'),
          opacity: s.admin ? 0.9 : (s.reserved ? 0.6 : 1) }, svg);
      }
      const mid = angle + sweep / 2;
      if (s.dept) {
        const pos = polar(cx, cy, r + 40, mid);
        mascot(svg, pos.x, pos.y, 15, s.color, s.name);
        if (sweep > 14) {
          const lp = polar(cx, cy, r * 0.58, mid);
          const lbl = el('text', { x: lp.x, y: lp.y, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'700', 'font-size':'10', fill:'#fff' }, svg);
          lbl.textContent = Math.round(s.pct) + '%';
        }
      }
      angle += sweep;
    });

    el('circle', { cx, cy, r: r + 3, fill:'none', stroke:'currentColor', 'stroke-width':2, opacity:0.35 }, svg);

    let footY = 268;
    const reserved = slices.find(s => s.reserved);
    if (reserved) {
      const rt = el('text', { x:150, y:footY, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'8.5', fill:'var(--ink-dim)' }, svg);
      rt.textContent = `Reserved (off-limits): ${Math.round(reserved.pct)}%`;
      footY += 11;
    }
    const admin = slices.find(s => s.admin);
    if (admin) {
      const at = el('text', { x:150, y:footY, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'700', 'font-size':'8.5', fill:'var(--accent)' }, svg);
      at.textContent = `Administrative Fee (phantom): ${Math.round(admin.pct)}%`;
    }
  }

  function describeLive(roundIdx, modPct, twistCount){
    const slices = computeSlices(roundIdx, modPct, twistCount);
    const mod = slices.find(s => s.dept && s.name === DEPARTMENTS[MOD_IDX].name);
    const pct = mod.pct.toFixed(1);
    let flavor;
    if (mod.pct < 2) flavor = 'gets nothing and a fruit basket';
    else if (mod.pct < 8) flavor = `gets ${pct}% of the pie and a strongly worded thank-you card`;
    else if (mod.pct < 20) flavor = `gets a modest ${pct}% slice and has to say it is "plenty"`;
    else if (mod.pct < 40) flavor = `walks away with a healthy ${pct}% and a photo op`;
    else flavor = `somehow ends up with ${pct}% of the pie and will not explain how`;
    let extra = '';
    const reserved = slices.find(s => s.reserved);
    if (reserved) extra += ` Meanwhile ${reserved.pct.toFixed(0)}% sits Reserved, untouched.`;
    const admin = slices.find(s => s.admin);
    if (admin) extra += ` A phantom ${admin.pct.toFixed(0)}% Administrative Fee has appeared from nowhere and is eating everyone's share.`;
    return `${DEPARTMENTS[MOD_IDX].name} now ${flavor}.${extra}`;
  }

  function updateCaption(i){
    const capEl = document.getElementById('cap' + i);
    if (state.touched[i] || state.twists > 0) {
      capEl.textContent = describeLive(i, currentModPct(i), state.twists);
    } else {
      capEl.textContent = CAPTIONS[i];
    }
  }

  function renderPanel(i){
    const svg = document.getElementById('art' + i);
    drawScene(svg, i, currentModPct(i), state.twists);
    updateCaption(i);
    document.getElementById('stamp' + i).textContent = i <= state.revealed - 1 ? ['SERVED','CARVED','RATIONED'][i] : '';
    const cover = document.getElementById('cover' + i);
    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 activePanelIndex(){
    return state.revealed > 0 ? Math.min(state.revealed, 3) - 1 : -1;
  }

  function syncSlider(){
    const slider = document.getElementById('pieSlider');
    const valEl = document.getElementById('sliderVal');
    const idx = activePanelIndex();
    if (idx < 0) {
      slider.disabled = true;
      slider.value = 0;
      valEl.textContent = '— (process Panel 1 first)';
      return;
    }
    slider.disabled = false;
    const pct = currentModPct(idx);
    slider.value = Math.round(pct);
    valEl.textContent = `Panel ${idx+1}: ${Math.round(pct)}%`;
  }

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

  function syncButtons(){
    const nextBtn = document.getElementById('nextBtn');
    if (state.revealed >= 3) { nextBtn.textContent = 'All 3 rounds carved ✓'; 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 rounds are already on the table. 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);
    if (state.revealed < 3) renderPanel(state.revealed);
    say('ok', `Panel ${state.revealed} processed. Drag the slider to re-carve it.`);
    syncButtons();
    syncSlider();
    if (state.revealed >= 3) say('ok', 'All 3 rounds carved. Press Plot Twist! to see who invents a new fee.');
  }

  function twist(){
    if (state.revealed < 3) { say('err', 'ERROR PT-409 · Premature veto — carve all 3 rounds first.'); return; }
    state.twists++;
    for (let i=0;i<3;i++) renderPanel(i);
    syncSlider();
    say('ok', `Plot twist #${state.twists}: a phantom Administrative Fee has appeared and is eating everyone's share.`);
  }

  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(); } });
  }

  const slider = document.getElementById('pieSlider');
  slider.addEventListener('input', () => {
    const idx = activePanelIndex();
    if (idx < 0) return;
    const val = Number(slider.value);
    state.sliderOverride[idx] = val;
    state.touched[idx] = true;
    renderPanel(idx);
    document.getElementById('sliderVal').textContent = `Panel ${idx+1}: ${val}%`;
    say('info', describeLive(idx, val, state.twists));
  });

  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', 'The pie has arrived. Press N or click Panel 1 to begin serving.');
})();
</script>
</body>
</html>