← 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 &middot; 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">&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; 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 &middot; 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 &middot; 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/&rarr; next panel &middot; T plot twist (erects the next mini-statue) &middot; 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>