// src/pages/agent-office/canvas/OverlayRenderer.js /** * 캔버스 위 오버레이 렌더링: * - 이름 라벨 (항상) * - 상태 배지 (항상) * - 말풍선 (waiting 상태에서만) * - 알림 배지 (notification > 0 일 때) */ const STATE_BADGE = { idle: { text: 'idle', bg: '#374151', fg: '#9ca3af' }, working: { text: 'working', bg: '#1e3a5f', fg: '#60a5fa' }, waiting: { text: 'waiting', bg: '#92400e', fg: '#fbbf24' }, reporting: { text: 'reporting', bg: '#1e3a5f', fg: '#60a5fa' }, break: { text: 'break', bg: '#065f46', fg: '#34d399' } }; export class OverlayRenderer { constructor() { this._bubbleAlpha = new Map(); // agentId → alpha (fade in/out) } draw(ctx, sprite, theme, zoom, panX, panY, tileSize) { const ts = tileSize * zoom; const centerX = sprite.x * ts + panX + ts / 2; const topY = sprite.y * ts + panY - ts * 0.3; const fontSize = Math.max(10, 11 * zoom / 2); const smallFontSize = Math.max(8, 9 * zoom / 2); // 1. 이름 라벨 ctx.font = `bold ${fontSize}px 'Courier New', monospace`; ctx.textAlign = 'center'; ctx.fillStyle = sprite.meta.color; ctx.fillText(sprite.meta.displayName, centerX, topY + ts * 1.85); // 2. 상태 배지 const badge = STATE_BADGE[sprite.state] || STATE_BADGE.idle; const badgeText = badge.text; ctx.font = `${smallFontSize}px 'Courier New', monospace`; const badgeW = ctx.measureText(badgeText).width + 8; const badgeH = smallFontSize + 4; const badgeX = centerX - badgeW / 2; const badgeY = topY + ts * 1.9; ctx.fillStyle = badge.bg; this._roundRect(ctx, badgeX, badgeY, badgeW, badgeH, 3); ctx.fill(); ctx.fillStyle = badge.fg; ctx.textAlign = 'center'; ctx.fillText(badgeText, centerX, badgeY + badgeH - 3); // 3. 말풍선 (waiting 상태에서만) if (sprite.state === 'waiting') { this._drawBubble(ctx, sprite, centerX, topY - ts * 0.2, zoom); } // 4. 알림 배지 if (sprite.notificationCount > 0) { this._drawNotificationBadge(ctx, centerX + ts * 0.5, topY + ts * 0.2, sprite.notificationCount, zoom); } } _drawBubble(ctx, sprite, x, y, zoom) { const text = '승인 대기!'; const fontSize = Math.max(10, 11 * zoom / 2); ctx.font = `bold ${fontSize}px 'Courier New', monospace`; const tw = ctx.measureText(text).width; const pw = tw + 16; const ph = fontSize + 12; const px = x - pw / 2; const py = y - ph; // 말풍선 배경 ctx.fillStyle = '#fbbf24'; this._roundRect(ctx, px, py, pw, ph, 6); ctx.fill(); // 꼬리 삼각형 ctx.beginPath(); ctx.moveTo(x - 5, py + ph); ctx.lineTo(x + 5, py + ph); ctx.lineTo(x, py + ph + 6); ctx.closePath(); ctx.fill(); // 텍스트 ctx.fillStyle = '#000000'; ctx.textAlign = 'center'; ctx.fillText(text, x, py + ph - 5); } _drawNotificationBadge(ctx, x, y, count, zoom) { const r = Math.max(7, 8 * zoom / 2); ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = `bold ${r}px sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(count > 9 ? '9+' : String(count), x, y); ctx.textBaseline = 'alphabetic'; } _roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); } }