feat(agent-office): add overlay renderer with labels, badges, and speech bubbles
This commit is contained in:
@@ -1,59 +1,122 @@
|
|||||||
// src/pages/agent-office/canvas/OverlayRenderer.js
|
// src/pages/agent-office/canvas/OverlayRenderer.js
|
||||||
// Phase 3 stub — full implementation in Phase 3
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 오버레이 렌더러 (Phase 3에서 구현)
|
* 캔버스 위 오버레이 렌더링:
|
||||||
* 에이전트 위의 상태 아이콘, 이름 레이블, 알림 배지, 말풍선 등
|
* - 이름 라벨 (항상)
|
||||||
|
* - 상태 배지 (항상)
|
||||||
|
* - 말풍선 (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 {
|
export class OverlayRenderer {
|
||||||
/**
|
constructor() {
|
||||||
* 에이전트 오버레이 렌더링
|
this._bubbleAlpha = new Map(); // agentId → alpha (fade in/out)
|
||||||
* @param {CanvasRenderingContext2D} ctx
|
|
||||||
* @param {object} sprite - AgentSprite 인스턴스
|
|
||||||
* @param {object} theme - 테마 객체
|
|
||||||
* @param {number} scale - 줌 레벨
|
|
||||||
* @param {number} offsetX - 패닝 X
|
|
||||||
* @param {number} offsetY - 패닝 Y
|
|
||||||
* @param {number} tileSize - 타일 크기
|
|
||||||
*/
|
|
||||||
draw(ctx, sprite, theme, scale, offsetX, offsetY, tileSize) {
|
|
||||||
// Phase 3에서 구현 예정
|
|
||||||
// 현재는 기본 레이블만 표시
|
|
||||||
if (!sprite || sprite.x == null) return;
|
|
||||||
|
|
||||||
const ts = tileSize * scale;
|
|
||||||
const cx = sprite.x * ts + offsetX + ts / 2;
|
|
||||||
const cy = sprite.y * ts + offsetY;
|
|
||||||
|
|
||||||
// 에이전트 이름 레이블
|
|
||||||
if (sprite.meta && sprite.meta.displayName) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.font = `${Math.max(8, 9 * scale)}px monospace`;
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillStyle = theme.text.secondary;
|
|
||||||
ctx.fillText(sprite.meta.displayName, cx, cy - 4 * scale);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 알림 배지
|
draw(ctx, sprite, theme, zoom, panX, panY, tileSize) {
|
||||||
if (sprite.notificationCount > 0) {
|
const ts = tileSize * zoom;
|
||||||
const bx = cx + ts * 0.3;
|
const centerX = sprite.x * ts + panX + ts / 2;
|
||||||
const by = cy - ts * 0.5;
|
const topY = sprite.y * ts + panY - ts * 0.3;
|
||||||
const r = Math.max(5, 6 * scale);
|
|
||||||
ctx.save();
|
const fontSize = Math.max(10, 11 * zoom / 2);
|
||||||
ctx.beginPath();
|
const smallFontSize = Math.max(8, 9 * zoom / 2);
|
||||||
ctx.arc(bx, by, r, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = '#ef4444';
|
// 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.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.fillStyle = '#ffffff';
|
||||||
ctx.font = `bold ${Math.max(7, 7 * scale)}px monospace`;
|
ctx.font = `bold ${r}px sans-serif`;
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText(
|
ctx.fillText(count > 9 ? '9+' : String(count), x, y);
|
||||||
sprite.notificationCount > 9 ? '9+' : String(sprite.notificationCount),
|
ctx.textBaseline = 'alphabetic';
|
||||||
bx, by
|
|
||||||
);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user