123 lines
3.7 KiB
JavaScript
123 lines
3.7 KiB
JavaScript
// 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();
|
|
}
|
|
}
|