← back to Crazy News Channel
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 · 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">← Cartoon Desk</a>
</div>
</header>
<main>
<div class="strip" id="strip">
<figure class="panel" data-i="0">
<div class="phead"><span>Panel 1 · 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 · 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 · 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/→ next panel · T plot twist · click a panel to advance · 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>