| 22 | } |
| 23 | |
| 24 | function paint(ctx, w, h) { |
| 25 | // Cream paper background + vignette. |
| 26 | ctx.fillStyle = theme.widget_base; |
| 27 | ctx.fillRect(0, 0, w, h); |
| 28 | |
| 29 | // Header. |
| 30 | const pairs = Math.max(trails.length, Math.floor(datasets.length / 2)); |
| 31 | ctx.fillStyle = theme.widget_text; |
| 32 | ctx.font = "bold 11px sans-serif"; |
| 33 | ctx.textAlign = "start"; |
| 34 | ctx.fillText("XY SCOPE", 14, 18); |
| 35 | ctx.fillStyle = theme.placeholder_text; |
| 36 | ctx.font = "10px sans-serif"; |
| 37 | ctx.textAlign = "end"; |
| 38 | ctx.fillText(pairs + (pairs === 1 ? " pair" : " pairs"), w - 14, 18); |
| 39 | ctx.fillStyle = theme.widget_border; |
| 40 | ctx.fillRect(14, 22, w - 28, 1); |
| 41 | |
| 42 | if (pairs === 0) { |
| 43 | ctx.fillStyle = theme.placeholder_text; |
| 44 | ctx.font = "12px sans-serif"; |
| 45 | ctx.textAlign = "center"; |
| 46 | ctx.fillText("Add datasets in (X, Y) pairs", w / 2, h / 2); |
| 47 | return; |
| 48 | } |
| 49 | |
| 50 | // Layout: square-ish grid of cards. |
| 51 | const cols = Math.ceil(Math.sqrt(pairs * w / h)); |
| 52 | const rows = Math.ceil(pairs / cols); |
| 53 | const padX = 14; |
| 54 | const padTop = 30; |
| 55 | const padBot = 14; |
| 56 | const gap = 8; |
| 57 | const cw = (w - padX * 2 - gap * (cols - 1)) / cols; |
| 58 | const ch = (h - padTop - padBot - gap * (rows - 1)) / rows; |
| 59 | |
| 60 | for (let p = 0; p < pairs; ++p) { |
| 61 | const r = Math.floor(p / cols); |
| 62 | const c = p % cols; |
| 63 | const x0 = padX + c * (cw + gap); |
| 64 | const y0 = padTop + r * (ch + gap); |
| 65 | const cx = x0 + cw * 0.5; |
| 66 | const cy = y0 + ch * 0.5 + 6; |
| 67 | const sz = Math.min(cw, ch - 16) * 0.42; |
| 68 | |
| 69 | // Card. |
| 70 | ctx.fillStyle = theme.widget_border; |
| 71 | ctx.fillRect(x0 + 1, y0 + 2, cw, ch); |
| 72 | ctx.fillStyle = theme.alternate_base; |
| 73 | ctx.fillRect(x0, y0, cw, ch); |
| 74 | ctx.strokeStyle = theme.widget_border; |
| 75 | ctx.lineWidth = 1; |
| 76 | ctx.strokeRect(x0 + 0.5, y0 + 0.5, cw - 1, ch - 1); |
| 77 | |
| 78 | // Mini header. |
| 79 | const dsX = datasets[p * 2]; |
| 80 | const dsY = datasets[p * 2 + 1]; |
| 81 | ctx.fillStyle = theme.placeholder_text; |