| 93 | } |
| 94 | |
| 95 | private render(ctx: CanvasRenderingContext2D): void { |
| 96 | // No live stream or clock yet (e.g. a preview still) means the bar hasn't started: hold at 0%. |
| 97 | const elapsed = this.stream && this.clockStarted ? performance.now() - this.startedAt : 0; |
| 98 | const finished = elapsed >= FILL_MS; |
| 99 | const pct = finished ? 100 : Math.round((elapsed / FILL_MS) * 100); |
| 100 | |
| 101 | // Backdrop — a plain "app window" look, not the Observer brand, so it reads as |
| 102 | // "some other app on your screen" rather than part of Observer's own UI. |
| 103 | ctx.fillStyle = '#1e2129'; |
| 104 | ctx.fillRect(0, 0, WIDTH, HEIGHT); |
| 105 | |
| 106 | ctx.fillStyle = '#2a2e38'; |
| 107 | ctx.fillRect(0, 0, WIDTH, 48); |
| 108 | ctx.fillStyle = '#e94b3c'; |
| 109 | ctx.beginPath(); ctx.arc(24, 24, 7, 0, Math.PI * 2); ctx.fill(); |
| 110 | ctx.fillStyle = '#f1c40f'; |
| 111 | ctx.beginPath(); ctx.arc(48, 24, 7, 0, Math.PI * 2); ctx.fill(); |
| 112 | ctx.fillStyle = '#2ecc71'; |
| 113 | ctx.beginPath(); ctx.arc(72, 24, 7, 0, Math.PI * 2); ctx.fill(); |
| 114 | |
| 115 | ctx.fillStyle = '#8b92a3'; |
| 116 | ctx.font = '600 15px -apple-system, BlinkMacSystemFont, sans-serif'; |
| 117 | ctx.textAlign = 'center'; |
| 118 | ctx.textBaseline = 'middle'; |
| 119 | ctx.fillText('Downloader.app', WIDTH / 2, 24); |
| 120 | |
| 121 | // Label |
| 122 | ctx.fillStyle = '#e5e7eb'; |
| 123 | ctx.font = '600 26px -apple-system, BlinkMacSystemFont, sans-serif'; |
| 124 | ctx.fillText(finished ? 'Download complete' : 'A reaaaally long progress bar', WIDTH / 2, HEIGHT / 2 - 60); |
| 125 | |
| 126 | // Progress bar track |
| 127 | const barW = 640, barH = 28, barX = (WIDTH - barW) / 2, barY = HEIGHT / 2 - 14; |
| 128 | ctx.fillStyle = '#333844'; |
| 129 | this.roundRect(ctx, barX, barY, barW, barH, 14); |
| 130 | ctx.fill(); |
| 131 | |
| 132 | // Progress bar fill |
| 133 | const fillW = Math.max(barH, (pct / 100) * barW); |
| 134 | ctx.fillStyle = finished ? '#2ecc71' : '#6366f1'; |
| 135 | this.roundRect(ctx, barX, barY, fillW, barH, 14); |
| 136 | ctx.fill(); |
| 137 | |
| 138 | // Percentage |
| 139 | ctx.fillStyle = '#e5e7eb'; |
| 140 | ctx.font = '600 20px -apple-system, BlinkMacSystemFont, sans-serif'; |
| 141 | ctx.fillText(`${pct}%`, WIDTH / 2, barY + barH + 40); |
| 142 | } |
| 143 | |
| 144 | private roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number): void { |
| 145 | const radius = Math.min(r, h / 2, w / 2); |