// ===================================================================== // Narrative arc — Task 4 // Shows dramatic-tension curve per act for the focused play (or // overlay curves for visible plays). Annotated with selected character's // presence per act. // ===================================================================== function NarrativeArc({ plays, focusPlay, selectedChar, selectedCharData, hoverChar, themeFilter }) { const [wrap, size] = useSize(); const W = size.w || 720, H = size.h || 200; const pad = { l: 56, r: 28, t: 26, b: 38 }; const iw = W - pad.l - pad.r; const ih = H - pad.t - pad.b; // smooth curve through points function smoothPath(pts) { if (pts.length < 2) return ""; let d = `M ${pts[0][0]} ${pts[0][1]}`; for (let i = 0; i < pts.length - 1; i++) { const p0 = pts[i-1] || pts[i]; const p1 = pts[i], p2 = pts[i+1], p3 = pts[i+2] || pts[i+1]; const cp1x = p1[0] + (p2[0] - p0[0]) / 6; const cp1y = p1[1] + (p2[1] - p0[1]) / 6; const cp2x = p2[0] - (p3[0] - p1[0]) / 6; const cp2y = p2[1] - (p3[1] - p1[1]) / 6; d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2[0]} ${p2[1]}`; } return d; } if (!plays.length) return null; // visible plays curve overlay; focused gets full opacity const visible = plays; const focused = focusPlay ? plays.find(p => p.id === focusPlay) : null; // x by act index normalized (0..1) so plays of different act counts align function pts(play) { const n = play.tension.length; return play.tension.map((v, i) => [ pad.l + (i / (n - 1)) * iw, pad.t + (1 - v) * ih, ]); } return (