← back to Crazy News Channel
cartoons/20260926-statue-selfie-line.html
328 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Line for the Statue for the Line — P24 Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: the queue to photograph a roadside attraction gets so popular a statue is erected of the queue, which draws a queue of its own." />
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body data-mood="entertainment" class="cartoon-page cartoon-page-wide">
<header>
<div>
<div class="kicker">P24 Cartoon Desk · Invented Satire</div>
<h1>The Line for the Statue for the Line</h1>
<p class="dek">An invented roadside attraction draws a photo line. The line draws a statue. The statue draws a line. Nobody has reached the front.</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 · The Attraction</span><span id="stamp0"></span></div>
<svg class="art" id="art0" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Panel 1, not yet revealed."></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 Unveiling</span><span id="stamp1"></span></div>
<svg class="art" id="art1" viewBox="0 0 300 300" role="img" tabindex="-1" aria-label="Panel 2, in queue."></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 · The Queue of Queues</span><span id="stamp2"></span></div>
<svg class="art" id="art2" viewBox="0 0 300 300" role="img" tabindex="-1" aria-label="Panel 3, in queue."></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">Unveil 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/→ next panel · T plot twist (erects the next mini-statue) · click a panel to advance</p>
</main>
<footer>The attraction, the town, the tour buses, and every statue-of-a-statue here are invented. No real monument, artist, or municipality is depicted. 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;
}
// prefers-reduced-motion: respected by construction — every panel is drawn as a
// static SVG with no transitions, transforms-over-time, or timers, so there is no
// motion to reduce. (Per TK-12226 this page adds no <style> block of its own, so
// an animation + its reduced-motion override would have nowhere to live anyway.)
// ---- invented roadside attractions — flat iconographic silhouettes, never a real monument ----
// `s0` is the per-shape hero scale: each silhouette has a different natural
// height, so this normalises them all to roughly fill the panel without
// colliding with the sign at the top.
const ATTRACTIONS = [
{ name: 'The Enormous Pinto Bean', short: 'Pinto Bean', color: '#b5302f', shape: 'bean', s0: 2.6 },
{ name: 'The Great Galvanized Pelican', short: 'Galvanized Pelican', color: '#0f6b52', shape: 'bird', s0: 2.0 },
{ name: "The World's Second-Largest Thermos", short: 'Second-Largest Thermos', color: '#c9a227', shape: 'thermos', s0: 1.9 },
{ name: 'The Colossal Wind-Chime', short: 'Colossal Wind-Chime', color: '#6b4fa0', shape: 'chime', s0: 2.1 },
];
let attrIdx = 0;
const state = { revealed: 0, spawns: 0 };
// ---------- drawing primitives ----------
function plinth(svg, cx, topY, w, h, color){
el('rect', { x: cx - w/2, y: topY, width: w, height: h, fill: color, 'fill-opacity': '0.15', stroke: color, 'stroke-width': 2.5 }, svg);
}
// The invented attraction itself, drawn as a flat silhouette on a plinth.
function attraction(svg, cx, baseY, scale, a){
const g = el('g', {}, svg);
const s = scale, c = a.color;
const topY = baseY - 18 * s;
plinth(svg, cx, topY, 46 * s, 18 * s, c);
if (a.shape === 'bean') {
el('path', { d: `M ${cx - 26*s} ${topY - 26*s} q ${12*s} ${-30*s} ${26*s} ${-26*s} q ${20*s} ${6*s} ${24*s} ${26*s} q ${-6*s} ${18*s} ${-26*s} ${18*s} q ${-20*s} ${0} ${-24*s} ${-18*s} z`,
fill: c, 'fill-opacity': '0.55', stroke: c, 'stroke-width': 2.5*s }, g);
el('path', { d: `M ${cx - 12*s} ${topY - 34*s} q ${12*s} ${8*s} ${22*s} ${2*s}`, fill: 'none', stroke: c, 'stroke-width': 2*s, 'stroke-linecap': 'round' }, g);
} else if (a.shape === 'bird') {
el('ellipse', { cx, cy: topY - 24*s, rx: 20*s, ry: 24*s, fill: c, 'fill-opacity': '0.5', stroke: c, 'stroke-width': 2.5*s }, g);
el('circle', { cx: cx + 4*s, cy: topY - 50*s, r: 10*s, fill: c, 'fill-opacity': '0.7', stroke: c, 'stroke-width': 2*s }, g);
el('path', { d: `M ${cx + 13*s} ${topY - 50*s} l ${18*s} ${5*s} l ${-18*s} ${5*s} z`, fill: c }, g);
} else if (a.shape === 'thermos') {
el('rect', { x: cx - 16*s, y: topY - 58*s, width: 32*s, height: 58*s, rx: 7*s, fill: c, 'fill-opacity': '0.5', stroke: c, 'stroke-width': 2.5*s }, g);
el('rect', { x: cx - 19*s, y: topY - 68*s, width: 38*s, height: 12*s, rx: 4*s, fill: c, 'fill-opacity': '0.8', stroke: c, 'stroke-width': 2*s }, g);
el('line', { x1: cx - 16*s, y1: topY - 30*s, x2: cx + 16*s, y2: topY - 30*s, stroke: c, 'stroke-width': 2*s }, g);
} else {
el('rect', { x: cx - 24*s, y: topY - 62*s, width: 48*s, height: 7*s, rx: 3*s, fill: c, 'fill-opacity': '0.8', stroke: c, 'stroke-width': 2*s }, g);
[-14, 0, 14].forEach((dx, i) => {
el('line', { x1: cx + dx*s, y1: topY - 55*s, x2: cx + dx*s, y2: topY - (28 - i*6)*s, stroke: c, 'stroke-width': 2*s }, g);
el('rect', { x: cx + (dx - 4)*s, y: topY - (28 - i*6)*s, width: 8*s, height: 16*s, rx: 2*s, fill: c, 'fill-opacity': '0.55', stroke: c, 'stroke-width': 1.6*s }, g);
});
}
return g;
}
// One flat visitor icon; `cam` adds a raised camera, `looking` faces left/right.
function visitor(svg, cx, baseY, scale, color, cam){
const g = el('g', {}, svg);
const s = scale;
el('circle', { cx, cy: baseY - 20*s, r: 5.5*s, fill: color }, g);
el('path', { d: `M ${cx - 7*s} ${baseY} q ${0} ${-13*s} ${7*s} ${-13*s} q ${7*s} ${0} ${7*s} ${13*s}`,
fill: color, 'fill-opacity': '0.45', stroke: color, 'stroke-width': 1.8*s }, g);
if (cam) el('rect', { x: cx + 4*s, y: baseY - 26*s, width: 9*s, height: 6.5*s, rx: 1.5*s, fill: 'none', stroke: color, 'stroke-width': 1.8*s }, g);
return g;
}
// A queue of visitors walking toward `towardX`.
function queue(svg, startX, baseY, count, step, scale, color, cam){
const g = el('g', {}, svg);
for (let i = 0; i < count; i++) visitor(g, startX + i*step, baseY, scale, color, cam && i % 2 === 0);
return g;
}
// A *statue of a queue*: the queue silhouette, frozen, up on its own plinth.
function queueStatue(svg, cx, baseY, scale, color, people){
const g = el('g', {}, svg);
const s = scale;
plinth(g, cx, baseY - 11*s, (14*people + 10)*s, 11*s, color);
const startX = cx - (people - 1) * 7 * s;
for (let i = 0; i < people; i++) {
visitor(g, startX + i*14*s, baseY - 11*s, 0.85*s, color, i % 2 === 0);
}
return g;
}
function ground(svg, y){
el('line', { x1: 8, y1: y, x2: 292, y2: y, stroke: 'var(--rule)', 'stroke-width': 2, opacity: 0.55 }, svg);
}
function caption(svg, y, text, opts){
opts = opts || {};
const t = el('text', { x: 150, y, 'text-anchor': 'middle', 'font-size': opts.size || '12',
'font-weight': opts.bold ? '700' : '400', fill: opts.dim ? 'var(--ink-dim)' : 'currentColor' }, svg);
t.textContent = text;
return t;
}
function signpost(svg, cx, y, label, color){
const g = el('g', {}, svg);
// Box is sized off the label so longer signs ("STATUE OF THE LINE") don't
// spill outside their own dashed border.
const w = Math.min(276, Math.max(76, label.length * 6.1 + 20));
el('rect', { x: cx - w/2, y: y - 13, width: w, height: 19, rx: 3, fill: 'none', stroke: color, 'stroke-width': 2, 'stroke-dasharray': '4 3' }, g);
const t = el('text', { x: cx, y: y + 1, 'text-anchor': 'middle', 'font-size': '9', 'font-weight': '800', fill: color }, g);
t.textContent = label;
return g;
}
// ---------- panels ----------
// Raises a statue onto a display pedestal reaching down to the ground line.
function pedestal(svg, cx, topY, groundY, w, color){
el('rect', { x: cx - w/2, y: topY, width: w, height: groundY - topY, fill: color, 'fill-opacity': '0.1', stroke: color, 'stroke-width': 2 }, svg);
}
const GY = 252; // shared ground line, so all three panels sit on the same horizon
function panel0(svg, a){
svg.textContent = '';
signpost(svg, 150, 40, 'PHOTO LINE', a.color);
ground(svg, GY);
attraction(svg, 78, GY, a.s0, a);
queue(svg, 158, GY, 4, 28, 1.7, 'currentColor', true);
caption(svg, 284, 'Wait time: 40 minutes. Everyone agrees it is worth it.', { dim: true });
}
function panel1(svg, a){
svg.textContent = '';
signpost(svg, 150, 40, 'STATUE OF THE LINE', a.color);
ground(svg, GY);
// the original attraction, now upstaged and receding
attraction(svg, 38, GY, a.s0 * 0.44, a);
// the original line, still standing, now itself the thing being photographed
queue(svg, 74, GY, 3, 19, 1.35, 'var(--ink-dim)', false);
// the brand-new statue OF that line, up on a pedestal
pedestal(svg, 238, 190, GY, 32, a.color);
queueStatue(svg, 238, 190, 1.3, a.color, 4);
// and the line that has already formed to photograph it
queue(svg, 140, GY, 3, 18, 1.3, 'currentColor', true);
caption(svg, 284, 'The line is now the attraction. A plaque is ordered.', { dim: true });
}
function panel2(svg, a, spawns){
svg.textContent = '';
const depth = 2 + spawns; // statues-of-a-queue standing in the row
signpost(svg, 150, 40, 'QUEUE OF QUEUES', a.color);
ground(svg, GY);
// Each statue depicts the queue for the statue to its left; they recede to the right.
let cx = 60, s = 1.25, people = 5, drawn = 0;
for (let d = 0; d < depth; d++) {
const w = (14 * people + 10) * s;
if (cx + w/2 > 230 || s < 0.3) break; // leave the right edge for the live queue
pedestal(svg, cx, GY - 44*s, GY, Math.max(12, 22*s), d % 2 === 0 ? a.color : 'var(--seal)');
queueStatue(svg, cx, GY - 44*s, s, d % 2 === 0 ? a.color : 'var(--seal)', people);
drawn++;
const nextS = s * 0.74, nextPeople = Math.max(2, people - 1);
cx += w/2 + ((14 * nextPeople + 10) * nextS)/2 + 8;
s = nextS; people = nextPeople;
}
// the only living people left in the picture: the line for the smallest statue
queue(svg, 242, GY, 3, 14, 1.05, 'currentColor', true);
if (drawn < depth) caption(svg, 268, `(+${depth - drawn} more off-frame)`, { size: '9', dim: true });
caption(svg, 284, `Statues of lines standing: ${depth}. Lines for them: ${depth}.`, { bold: true });
}
// ---------- captions / a11y text ----------
function captions(a, depth){
return [
`Visitors queue down the shoulder of an invented county highway to photograph ${a.name}, an attraction of no fixed purpose.`,
`The photo line outgrows the statue, so the invented town unveils a statue of the line — which immediately forms a line of its own.`,
`Queue-of-queues count: ${depth}. Each new statue commemorates the people waiting for the last one.`,
];
}
function alts(a, depth){
return [
`Flat cartoon: ${a.name} on a plinth beside a highway, with seven visitors queued up holding cameras under a sign reading photo line.`,
`Flat cartoon: the original photo line now stands next to a smaller statue of that same line on its own plinth, with four new visitors queueing to photograph the statue of the line.`,
`Flat cartoon: a receding row of ${depth} progressively smaller statues, each depicting the queue for the one before it, each with a small queue of its own.`,
];
}
// ---------- render ----------
const STAMPS = ['PHOTOGRAPHED', 'UNVEILED', 'RECURSIVE'];
function renderPanel(i){
const a = ATTRACTIONS[attrIdx];
const depth = 2 + state.spawns;
const svg = document.getElementById('art' + i);
if (i === 0) panel0(svg, a);
if (i === 1) panel1(svg, a);
if (i === 2) panel2(svg, a, state.spawns);
const shown = i < state.revealed;
svg.setAttribute('aria-label', shown ? alts(a, depth)[i] : `Panel ${i+1}, not yet revealed.`);
document.getElementById('cap' + i).textContent = shown ? captions(a, depth)[i] : '';
document.getElementById('stamp' + i).textContent = shown ? STAMPS[i] : '';
const cover = document.getElementById('cover' + i);
cover.hidden = shown;
cover.disabled = i !== state.revealed;
if (i === state.revealed) cover.textContent = i === 0 ? 'Click, tap, or press N to begin' : 'Click, tap, or press N to unveil';
else if (!shown) cover.textContent = 'In queue';
svg.setAttribute('tabindex', i === state.revealed || (state.revealed >= 3 && i === 2) ? '0' : '-1');
}
function paintAll(){ for (let i = 0; i < 3; i++) renderPanel(i); syncButtons(); }
function syncButtons(){
const nextBtn = document.getElementById('nextBtn');
if (state.revealed >= 3) { nextBtn.textContent = 'All 3 panels unveiled ✓'; nextBtn.disabled = true; }
else { nextBtn.textContent = `Unveil 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) { spawn(); return; } // brief: clicking the finished strip erects the next mini-statue
state.revealed++;
paintAll();
if (state.revealed >= 3) say('ok', 'All 3 panels unveiled. Press Plot Twist! to erect the next statue of the line.');
else say('ok', `Panel ${state.revealed} unveiled.`);
const active = document.getElementById('art' + Math.min(state.revealed, 2));
if (active && document.activeElement && document.activeElement.classList.contains('cover')) active.focus();
}
// Plot twist = a new invented attraction takes over AND the recursion grows one statue deeper.
function twist(){
if (state.revealed < 3) { say('err', 'ERROR SL-409 · Premature twist — unveil all 3 panels first.'); return; }
attrIdx = (attrIdx + 1) % ATTRACTIONS.length;
spawn(true);
}
function spawn(fromTwist){
if (state.revealed < 3) { say('err', 'ERROR SL-409 · Nothing to commemorate yet — unveil all 3 panels first.'); return; }
state.spawns++;
paintAll();
const depth = 2 + state.spawns;
const a = ATTRACTIONS[attrIdx];
say('ok', fromTwist
? `Plot twist #${state.spawns}: the ${a.short} is the new headline attraction, and statue #${depth} of the line goes up. Queue-of-queues count: ${depth}.`
: `Statue #${depth} of the line unveiled. Queue-of-queues count: ${depth}.`);
}
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(); });
const art = document.getElementById('art' + i);
art.addEventListener('click', () => { if (i === state.revealed) next(); else if (state.revealed >= 3 && i === 2) spawn(); });
art.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
if (i === state.revealed) { e.preventDefault(); next(); }
else if (state.revealed >= 3 && i === 2) { e.preventDefault(); spawn(); }
});
}
document.addEventListener('keydown', (e) => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const tag = (e.target.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select' || e.target.isContentEditable) 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', 'Photo line open. Press N or click Panel 1 to begin.');
})();
</script>
</body>
</html>