Fix office layout, performance, and crash issues
Layout: agents now arrange in a horizontal grid (3-4 per row based on viewport width) instead of stacking vertically. Position calculation uses layoutPositions() which guarantees agents stay within the canvas. Performance: render loop throttled to 30fps instead of uncapped 60fps. Removed Math.random() from render path — building window lights now use a deterministic hash that changes slowly. Canvas dimensions only resize when clientWidth/clientHeight actually change. Crash fix: clearRect at frame start prevents accumulated draw state. setTransform instead of scale prevents compounding transforms. Animation cleanup properly cancels requestAnimationFrame on unmount.
This commit is contained in:
1 parent
2447bfba9c
commit
82e28111e9
2 files changed
+196
-249
No files matched your search
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useRef, useMemo, useCallback } from "react";
|
||||
import { useEffect, useRef, useMemo } from "react";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId } from "./types";
|
||||
import { drawScene, type OfficeAgent } from "./office/office-renderer";
|
||||
import { drawScene, layoutPositions, type OfficeAgent } from "./office/office-renderer";
|
||||
|
||||
const FACE_COLORS = ["#4a90d9", "#50b866", "#9b59b6", "#e67e22", "#e74c3c", "#3498db", "#1abc9c", "#f39c12"];
|
||||
const SHIRT_COLORS = ["#5b8dd9", "#d95b7a", "#5bbf6f", "#d9a65b", "#9b6fd9", "#5bc4c4", "#d97b5b", "#7fd95b"];
|
||||
const FACE = ["#4a90d9", "#50b866", "#9b59b6", "#e67e22", "#e74c3c", "#1abc9c", "#f39c12", "#3498db"];
|
||||
const SHIRT = ["#5b8dd9", "#d95b7a", "#5bbf6f", "#d9a65b", "#9b6fd9", "#5bc4c4", "#d97b5b", "#7fd95b"];
|
||||
|
||||
function hash(s: string): number {
|
||||
let h = 0;
|
||||
@@ -12,24 +12,26 @@ function hash(s: string): number {
|
||||
return Math.abs(h);
|
||||
}
|
||||
|
||||
function pick(palette: string[], seed: string): string {
|
||||
return palette[hash(seed) % palette.length];
|
||||
}
|
||||
|
||||
export function OfficePage({ snapshot }: PageProps) {
|
||||
const orgId = activeOrgId(snapshot);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const agentsRef = useRef<OfficeAgent[]>([]);
|
||||
const animRef = useRef(0);
|
||||
const startRef = useRef(Date.now());
|
||||
const lastFrameRef = useRef(0);
|
||||
|
||||
const orgAgents = useMemo(() => snapshot.agents.filter((a) => a.orgId === orgId), [snapshot.agents, orgId]);
|
||||
const orgTasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const w = canvas.clientWidth;
|
||||
const positions = layoutPositions(orgAgents.length, w);
|
||||
const existing = agentsRef.current;
|
||||
|
||||
agentsRef.current = orgAgents.map((a, i) => {
|
||||
const prev = existing.find((e) => e.id === a.id);
|
||||
const pos = positions[i];
|
||||
const status = resolveStatus(snapshot.chatPresence, snapshot.taskRuns, a.id);
|
||||
const task = orgTasks.find((t) => t.assigneeAgentId === a.id && t.status === "in_progress")?.title ?? null;
|
||||
return {
|
||||
@@ -39,68 +41,81 @@ export function OfficePage({ snapshot }: PageProps) {
|
||||
department: a.department,
|
||||
status,
|
||||
task,
|
||||
x: prev?.x ?? 100 + i * 120,
|
||||
y: prev?.y ?? 200,
|
||||
targetX: prev?.targetX ?? 100 + i * 120,
|
||||
targetY: prev?.targetY ?? 200,
|
||||
deskX: 0,
|
||||
deskY: 0,
|
||||
faceColor: pick(FACE_COLORS, a.name),
|
||||
shirtColor: pick(SHIRT_COLORS, a.name + a.role),
|
||||
frame: prev?.frame ?? hash(a.id) % 100,
|
||||
facing: prev?.facing ?? 1,
|
||||
x: prev?.x ?? pos.x,
|
||||
y: prev?.y ?? pos.y,
|
||||
targetX: pos.x,
|
||||
targetY: pos.y,
|
||||
faceColor: FACE[hash(a.name) % FACE.length],
|
||||
shirtColor: SHIRT[hash(a.name + a.role) % SHIRT.length],
|
||||
frame: hash(a.id) % 100,
|
||||
};
|
||||
});
|
||||
}, [orgAgents, orgTasks, snapshot.chatPresence, snapshot.taskRuns]);
|
||||
|
||||
const render = useCallback(() => {
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.clientWidth;
|
||||
const h = canvas.clientHeight;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const time = Date.now() - startRef.current;
|
||||
const agents = agentsRef.current;
|
||||
let running = true;
|
||||
const FPS_INTERVAL = 1000 / 30;
|
||||
|
||||
for (const agent of agents) {
|
||||
const dx = agent.targetX - agent.x;
|
||||
const dy = agent.targetY - agent.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist > 1) {
|
||||
agent.x += (dx / dist) * 0.8;
|
||||
agent.y += (dy / dist) * 0.8;
|
||||
agent.frame += 0.1;
|
||||
agent.facing = dx > 0 ? 1 : -1;
|
||||
const tick = (now: number) => {
|
||||
if (!running) return;
|
||||
animRef.current = requestAnimationFrame(tick);
|
||||
|
||||
const elapsed = now - lastFrameRef.current;
|
||||
if (elapsed < FPS_INTERVAL) return;
|
||||
lastFrameRef.current = now - (elapsed % FPS_INTERVAL);
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.clientWidth;
|
||||
const h = canvas.clientHeight;
|
||||
if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
}
|
||||
}
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
drawScene(ctx, w, h, agents, time);
|
||||
animRef.current = requestAnimationFrame(render);
|
||||
for (const agent of agentsRef.current) {
|
||||
const dx = agent.targetX - agent.x;
|
||||
const dy = agent.targetY - agent.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist > 1) {
|
||||
agent.x += (dx / dist) * 0.7;
|
||||
agent.y += (dy / dist) * 0.7;
|
||||
agent.frame += 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
drawScene(ctx, w, h, agentsRef.current, now);
|
||||
};
|
||||
|
||||
animRef.current = requestAnimationFrame(tick);
|
||||
return () => { running = false; cancelAnimationFrame(animRef.current); };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
animRef.current = requestAnimationFrame(render);
|
||||
return () => cancelAnimationFrame(animRef.current);
|
||||
}, [render]);
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const w = canvas.clientWidth;
|
||||
const positions = layoutPositions(agentsRef.current.length, w);
|
||||
|
||||
useEffect(() => {
|
||||
const wander = setInterval(() => {
|
||||
for (const agent of agentsRef.current) {
|
||||
agentsRef.current.forEach((agent, i) => {
|
||||
const pos = positions[i];
|
||||
if (!pos) return;
|
||||
if (agent.status === "idle") {
|
||||
agent.targetX = agent.deskX + DESK_CENTER_OFFSET + (Math.random() * 100 - 50);
|
||||
agent.targetY = agent.deskY + 56 + (Math.random() * 40 - 20);
|
||||
agent.targetX = pos.x + ((hash(agent.id + String(Date.now()).slice(0, -3)) % 60) - 30);
|
||||
agent.targetY = pos.y + ((hash(agent.name + String(Date.now()).slice(0, -3)) % 30) - 15);
|
||||
} else {
|
||||
agent.targetX = agent.deskX + DESK_CENTER_OFFSET;
|
||||
agent.targetY = agent.deskY + 56;
|
||||
agent.targetX = pos.x;
|
||||
agent.targetY = pos.y;
|
||||
}
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(wander);
|
||||
}, []);
|
||||
|
||||
@@ -111,8 +126,6 @@ export function OfficePage({ snapshot }: PageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const DESK_CENTER_OFFSET = 50;
|
||||
|
||||
function resolveStatus(
|
||||
presence: Array<{ principalType: string; principalId: string; status: string }>,
|
||||
runs: Array<{ agentId: string | null; state: string }>,
|
||||
|
||||
@@ -9,200 +9,159 @@ export interface OfficeAgent {
|
||||
y: number;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
deskX: number;
|
||||
deskY: number;
|
||||
faceColor: string;
|
||||
shirtColor: string;
|
||||
frame: number;
|
||||
facing: number;
|
||||
}
|
||||
|
||||
const WALL_H = 120;
|
||||
const TILE = 24;
|
||||
const DESK_W = 100;
|
||||
const DESK_H = 48;
|
||||
const CHAR_H = 44;
|
||||
const WALL_H = 110;
|
||||
const DESK_W = 90;
|
||||
const DESK_SPACING = 140;
|
||||
const ROW_H = 160;
|
||||
|
||||
const WALL = "#3a3a4a";
|
||||
const WALL_TILE = "#424252";
|
||||
const FLOOR = "#4a4a5a";
|
||||
const FLOOR_TILE = "#52526a";
|
||||
const DESK_TOP = "#8b7355";
|
||||
const DESK_LEG = "#6b5335";
|
||||
const MONITOR_BODY = "#555";
|
||||
const MONITOR_SCREEN = "#222";
|
||||
const MONITOR_ACTIVE = "#1a3a2a";
|
||||
const FLOOR = "#484858";
|
||||
|
||||
export function drawScene(ctx: CanvasRenderingContext2D, w: number, h: number, agents: OfficeAgent[], time: number) {
|
||||
drawFloor(ctx, w, h);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
ctx.fillStyle = FLOOR;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
drawWall(ctx, w);
|
||||
drawWallDecorations(ctx, w, time);
|
||||
drawWallDecor(ctx, w, time);
|
||||
drawFloorGrid(ctx, w, h);
|
||||
|
||||
const groups = groupByDepartment(agents);
|
||||
let col = 0;
|
||||
let row = 0;
|
||||
const cols = Math.max(1, Math.floor((w - 40) / (DESK_W + 60)));
|
||||
for (const [, group] of groups) {
|
||||
for (const agent of group) {
|
||||
agent.deskX = 60 + (col % cols) * (DESK_W + 60);
|
||||
agent.deskY = WALL_H + 40 + row * (DESK_H + 90);
|
||||
if (agent.status !== "idle") {
|
||||
agent.targetX = agent.deskX + DESK_W / 2;
|
||||
agent.targetY = agent.deskY + DESK_H + 8;
|
||||
}
|
||||
col++;
|
||||
if (col % cols === 0) row++;
|
||||
}
|
||||
if (col % cols !== 0) { row++; col = Math.ceil(col / cols) * cols; }
|
||||
}
|
||||
const cols = Math.max(2, Math.floor((w - 40) / DESK_SPACING));
|
||||
|
||||
for (const [, group] of groups) {
|
||||
for (const agent of group) drawDeskUnit(ctx, agent, time);
|
||||
for (let i = 0; i < agents.length; i++) {
|
||||
const col = i % cols;
|
||||
const row = Math.floor(i / cols);
|
||||
const dx = 50 + col * DESK_SPACING;
|
||||
const dy = WALL_H + 30 + row * ROW_H;
|
||||
drawDesk(ctx, dx, dy, agents[i]);
|
||||
}
|
||||
|
||||
const sorted = [...agents].sort((a, b) => a.y - b.y);
|
||||
for (const agent of sorted) drawCharacter(ctx, agent, time);
|
||||
}
|
||||
|
||||
function drawFloor(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
||||
ctx.fillStyle = FLOOR;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.strokeStyle = FLOOR_TILE;
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x < w; x += TILE) { ctx.beginPath(); ctx.moveTo(x, WALL_H); ctx.lineTo(x, h); ctx.stroke(); }
|
||||
for (let y = WALL_H; y < h; y += TILE) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
|
||||
export function layoutPositions(count: number, viewW: number): Array<{ x: number; y: number }> {
|
||||
const cols = Math.max(2, Math.floor((viewW - 40) / DESK_SPACING));
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
x: 50 + (i % cols) * DESK_SPACING + DESK_W / 2,
|
||||
y: WALL_H + 30 + Math.floor(i / cols) * ROW_H + 70,
|
||||
}));
|
||||
}
|
||||
|
||||
function drawWall(ctx: CanvasRenderingContext2D, w: number) {
|
||||
ctx.fillStyle = WALL;
|
||||
ctx.fillRect(0, 0, w, WALL_H);
|
||||
ctx.strokeStyle = WALL_TILE;
|
||||
ctx.strokeStyle = "#42425a";
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x < w; x += TILE) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, WALL_H); ctx.stroke(); }
|
||||
for (let y = 0; y < WALL_H; y += TILE / 2) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
|
||||
for (let x = 0; x < w; x += 24) {
|
||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, WALL_H); ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < WALL_H; y += 12) {
|
||||
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.fillRect(0, WALL_H - 4, w, 4);
|
||||
ctx.fillRect(0, WALL_H - 3, w, 3);
|
||||
}
|
||||
|
||||
function drawWallDecorations(ctx: CanvasRenderingContext2D, w: number, time: number) {
|
||||
drawCityWindow(ctx, 30, 12, 90, 70);
|
||||
if (w > 400) drawCityWindow(ctx, w - 130, 12, 90, 70);
|
||||
drawClock(ctx, w / 2, 50, 28, time);
|
||||
if (w > 600) drawWhiteboard(ctx, w / 2 + 80, 14, 120, 76);
|
||||
if (w > 300) drawPainting(ctx, 160, 16, 60, 50);
|
||||
function drawFloorGrid(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.03)";
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x < w; x += 24) { ctx.beginPath(); ctx.moveTo(x, WALL_H); ctx.lineTo(x, h); ctx.stroke(); }
|
||||
for (let y = WALL_H; y < h; y += 24) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
|
||||
}
|
||||
|
||||
function drawCityWindow(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
|
||||
function drawWallDecor(ctx: CanvasRenderingContext2D, w: number, time: number) {
|
||||
drawWindow(ctx, 20, 10, 80, 60, time);
|
||||
if (w > 500) drawWindow(ctx, w - 110, 10, 80, 60, time);
|
||||
drawClock(ctx, Math.min(w / 2, 300), 45, 22, time);
|
||||
if (w > 400) drawBoard(ctx, Math.min(w / 2 + 60, w - 160), 12, 100, 70);
|
||||
}
|
||||
|
||||
function drawWindow(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, time: number) {
|
||||
ctx.fillStyle = "#1a1a3a";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "#666";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(x + w / 2, y); ctx.lineTo(x + w / 2, y + h); ctx.stroke();
|
||||
for (let bx = x + 8; bx < x + w - 8; bx += 14) {
|
||||
for (let by = y + h - 50; by < y + h - 6; by += 8) {
|
||||
ctx.fillStyle = Math.random() > 0.4 ? "#ff9" : "#553";
|
||||
ctx.fillRect(bx, by, 4, 4);
|
||||
const seed = Math.floor(time * 0.0001);
|
||||
for (let bx = x + 6; bx < x + w - 6; bx += 10) {
|
||||
for (let by = y + h - 40; by < y + h - 4; by += 7) {
|
||||
const on = ((bx * 7 + by * 13 + seed) % 5) > 1;
|
||||
ctx.fillStyle = on ? "#ffa" : "#334";
|
||||
ctx.fillRect(bx, by, 3, 3);
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = "#446";
|
||||
ctx.fillRect(x + 2, y + 2, 8, 8);
|
||||
ctx.fillStyle = "#ff8";
|
||||
ctx.beginPath(); ctx.arc(x + 6, y + 6, 4, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(x + w * 0.75, y + 12, 5, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
|
||||
function drawClock(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, time: number) {
|
||||
ctx.fillStyle = "#eee";
|
||||
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = "#333";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const a = (i / 12) * Math.PI * 2 - Math.PI / 2;
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.fillRect(cx + Math.cos(a) * (r - 5) - 1, cy + Math.sin(a) * (r - 5) - 1, 2, 2);
|
||||
}
|
||||
const min = (time * 0.001) % (Math.PI * 2);
|
||||
const hr = min / 12;
|
||||
ctx.strokeStyle = "#333";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(hr - Math.PI / 2) * (r * 0.5), cy + Math.sin(hr - Math.PI / 2) * (r * 0.5)); ctx.stroke();
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(min - Math.PI / 2) * (r * 0.7), cy + Math.sin(min - Math.PI / 2) * (r * 0.7)); ctx.stroke();
|
||||
ctx.stroke();
|
||||
const min = (time * 0.0005) % (Math.PI * 2);
|
||||
ctx.strokeStyle = "#333";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(min / 12 - Math.PI / 2) * r * 0.5, cy + Math.sin(min / 12 - Math.PI / 2) * r * 0.5); ctx.stroke();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(min - Math.PI / 2) * r * 0.7, cy + Math.sin(min - Math.PI / 2) * r * 0.7); ctx.stroke();
|
||||
}
|
||||
|
||||
function drawWhiteboard(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
|
||||
function drawBoard(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
|
||||
ctx.fillStyle = "#f5f5f0";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.strokeStyle = "#888";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.font = "bold 7px monospace";
|
||||
ctx.fillText("TODO", x + 6, y + 12);
|
||||
ctx.fillText("TODO", x + 5, y + 12);
|
||||
ctx.font = "6px monospace";
|
||||
ctx.fillStyle = "#555";
|
||||
const items = ["Review PRD", "Ship v1.0", "Fix bug #42", "Deploy staging"];
|
||||
items.forEach((item, i) => ctx.fillText(`• ${item}`, x + 6, y + 22 + i * 10));
|
||||
const items = ["Review PRD", "Ship v1.0", "Fix bug #42", "Deploy"];
|
||||
for (let i = 0; i < items.length; i++) ctx.fillText(`• ${items[i]}`, x + 5, y + 22 + i * 10);
|
||||
}
|
||||
|
||||
function drawPainting(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) {
|
||||
ctx.fillStyle = "#654";
|
||||
ctx.fillRect(x - 3, y - 3, w + 6, h + 6);
|
||||
ctx.fillStyle = "#1a2a3a";
|
||||
ctx.fillRect(x, y, w, h);
|
||||
ctx.fillStyle = "#2a4a3a";
|
||||
ctx.fillRect(x, y + h * 0.6, w, h * 0.4);
|
||||
ctx.fillStyle = "#ff8";
|
||||
ctx.beginPath(); ctx.arc(x + w * 0.7, y + h * 0.25, 6, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = "#888";
|
||||
ctx.font = "6px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("EMPLOYEE", x + w / 2, y + h + 10);
|
||||
ctx.fillText("OF THE MONTH", x + w / 2, y + h + 18);
|
||||
ctx.textAlign = "start";
|
||||
}
|
||||
|
||||
function drawDeskUnit(ctx: CanvasRenderingContext2D, agent: OfficeAgent, _time: number) {
|
||||
const dx = agent.deskX;
|
||||
const dy = agent.deskY;
|
||||
ctx.fillStyle = DESK_TOP;
|
||||
ctx.fillRect(dx, dy, DESK_W, 6);
|
||||
ctx.fillStyle = DESK_LEG;
|
||||
ctx.fillRect(dx + 4, dy + 6, 4, DESK_H - 6);
|
||||
ctx.fillRect(dx + DESK_W - 8, dy + 6, 4, DESK_H - 6);
|
||||
ctx.fillRect(dx + DESK_W / 2 - 2, dy + 6, 4, DESK_H - 10);
|
||||
|
||||
const mx = dx + DESK_W / 2 - 16;
|
||||
const my = dy - 28;
|
||||
ctx.fillStyle = MONITOR_BODY;
|
||||
ctx.fillRect(mx, my, 32, 24);
|
||||
ctx.fillStyle = agent.status === "active" ? MONITOR_ACTIVE : MONITOR_SCREEN;
|
||||
ctx.fillRect(mx + 2, my + 2, 28, 18);
|
||||
ctx.fillStyle = MONITOR_BODY;
|
||||
ctx.fillRect(mx + 13, my + 24, 6, 4);
|
||||
ctx.fillRect(mx + 8, my + 28, 16, 2);
|
||||
|
||||
if (agent.status === "active" && agent.task) {
|
||||
function drawDesk(ctx: CanvasRenderingContext2D, x: number, y: number, agent: OfficeAgent) {
|
||||
const active = agent.status === "active" || agent.status === "thinking";
|
||||
const mx = x + DESK_W / 2 - 14;
|
||||
const my = y - 26;
|
||||
ctx.fillStyle = "#555";
|
||||
ctx.fillRect(mx, my, 28, 20);
|
||||
ctx.fillStyle = active ? "#1a3a2a" : "#222";
|
||||
ctx.fillRect(mx + 2, my + 2, 24, 14);
|
||||
if (active && agent.task) {
|
||||
ctx.fillStyle = "#4f8";
|
||||
ctx.font = "5px monospace";
|
||||
const taskText = agent.task.slice(0, 18);
|
||||
ctx.fillText(taskText, mx + 4, my + 12);
|
||||
ctx.fillText(agent.task.slice(0, 16), mx + 3, my + 10);
|
||||
}
|
||||
ctx.fillStyle = "#555";
|
||||
ctx.fillRect(mx + 11, my + 20, 6, 3);
|
||||
ctx.fillRect(mx + 6, my + 23, 16, 2);
|
||||
|
||||
ctx.fillStyle = "#8b7355";
|
||||
ctx.fillRect(x, y, DESK_W, 5);
|
||||
ctx.fillStyle = "#6b5335";
|
||||
ctx.fillRect(x + 4, y + 5, 3, 30);
|
||||
ctx.fillRect(x + DESK_W - 7, y + 5, 3, 30);
|
||||
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.fillRect(dx + DESK_W / 2 - 12, dy - 2, 24, 3);
|
||||
ctx.fillRect(dx + DESK_W / 2 - 14, dy - 3, 2, 4);
|
||||
ctx.fillRect(dx + DESK_W / 2 + 12, dy - 3, 2, 4);
|
||||
ctx.fillRect(x + DESK_W / 2 - 10, y - 2, 20, 3);
|
||||
|
||||
if (agent.status !== "idle") {
|
||||
if (active) {
|
||||
ctx.fillStyle = "#86654a";
|
||||
ctx.fillRect(dx + DESK_W - 20, dy - 8, 8, 8);
|
||||
ctx.fillStyle = "#f5f5f0";
|
||||
ctx.fillRect(dx + DESK_W - 19, dy - 7, 6, 4);
|
||||
ctx.fillStyle = "#86654a";
|
||||
ctx.fillRect(dx + DESK_W - 17, dy - 2, 2, 2);
|
||||
ctx.fillRect(x + DESK_W - 16, y - 7, 7, 7);
|
||||
ctx.fillStyle = "#f5f0e8";
|
||||
ctx.fillRect(x + DESK_W - 15, y - 6, 5, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,89 +172,64 @@ function drawCharacter(ctx: CanvasRenderingContext2D, agent: OfficeAgent, time:
|
||||
const bob = active ? Math.sin(time * 0.003 + agent.frame) * 1.5 : 0;
|
||||
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.beginPath();
|
||||
ctx.arc(x - 10, y - CHAR_H + 6 + bob, 12, Math.PI, 0);
|
||||
ctx.lineTo(x + 10, y - CHAR_H + 6 + bob);
|
||||
ctx.arc(x + 10, y - CHAR_H + 6 + bob, 12, 0, Math.PI, true);
|
||||
ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(x - 9, y - 36 + bob, 10, Math.PI, 0); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(x + 9, y - 36 + bob, 10, Math.PI, 0, true); ctx.fill();
|
||||
ctx.fillRect(x - 9, y - 36 + bob, 18, 5);
|
||||
ctx.beginPath(); ctx.arc(x - 12, y - 34 + bob, 4, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(x + 12, y - 34 + bob, 4, 0, Math.PI * 2); ctx.fill();
|
||||
|
||||
ctx.fillStyle = agent.faceColor;
|
||||
ctx.beginPath(); ctx.arc(x, y - CHAR_H + 10 + bob, 10, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(x, y - 30 + bob, 9, 0, Math.PI * 2); ctx.fill();
|
||||
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(x - 4, y - CHAR_H + 8 + bob, 3, 3);
|
||||
ctx.fillRect(x + 1, y - CHAR_H + 8 + bob, 3, 3);
|
||||
ctx.fillRect(x - 4, y - 32 + bob, 3, 3);
|
||||
ctx.fillRect(x + 1, y - 32 + bob, 3, 3);
|
||||
ctx.fillStyle = "#111";
|
||||
ctx.fillRect(x - 3, y - CHAR_H + 9 + bob, 2, 2);
|
||||
ctx.fillRect(x + 2, y - CHAR_H + 9 + bob, 2, 2);
|
||||
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(x - 14, y - CHAR_H + 4 + bob, 4, 10);
|
||||
ctx.fillRect(x + 10, y - CHAR_H + 4 + bob, 4, 10);
|
||||
ctx.beginPath(); ctx.arc(x - 12, y - CHAR_H + 4 + bob, 5, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(x + 12, y - CHAR_H + 4 + bob, 5, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillRect(x - 3, y - 31 + bob, 2, 2);
|
||||
ctx.fillRect(x + 2, y - 31 + bob, 2, 2);
|
||||
|
||||
ctx.fillStyle = agent.shirtColor;
|
||||
roundRect(ctx, x - 10, y - CHAR_H + 20 + bob, 20, 16, 3);
|
||||
ctx.fillRect(x - 9, y - 20 + bob, 18, 14);
|
||||
|
||||
ctx.fillStyle = agent.faceColor;
|
||||
ctx.fillRect(x - 12, y - CHAR_H + 22 + bob, 4, 10);
|
||||
ctx.fillRect(x + 8, y - CHAR_H + 22 + bob, 4, 10);
|
||||
ctx.fillRect(x - 11, y - 18 + bob, 3, 8);
|
||||
ctx.fillRect(x + 8, y - 18 + bob, 3, 8);
|
||||
|
||||
ctx.fillStyle = "#444";
|
||||
ctx.fillRect(x - 6, y - CHAR_H + 36 + bob, 5, 8);
|
||||
ctx.fillRect(x + 1, y - CHAR_H + 36 + bob, 5, 8);
|
||||
ctx.fillRect(x - 5, y - 6 + bob, 4, 7);
|
||||
ctx.fillRect(x + 1, y - 6 + bob, 4, 7);
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.fillRect(x - 7, y - CHAR_H + 43 + bob, 6, 3);
|
||||
ctx.fillRect(x + 1, y - CHAR_H + 43 + bob, 6, 3);
|
||||
ctx.fillRect(x - 6, y + 1 + bob, 5, 3);
|
||||
ctx.fillRect(x + 1, y + 1 + bob, 5, 3);
|
||||
|
||||
if (active) {
|
||||
ctx.fillStyle = agent.status === "active" ? "#34d399" : "#818cf8";
|
||||
ctx.beginPath(); ctx.arc(x + 12, y - CHAR_H + 2 + bob, 3, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(x + 11, y - 40 + bob, 3, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
|
||||
if (agent.task && active) drawSpeechBubble(ctx, x, y - CHAR_H - 10, agent.task.slice(0, 24));
|
||||
if (agent.task && active) {
|
||||
const text = agent.task.slice(0, 20);
|
||||
ctx.font = "7px sans-serif";
|
||||
const tw = ctx.measureText(text).width + 10;
|
||||
const bx = x - tw / 2;
|
||||
const by = y - 50 + bob;
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bx + 3, by); ctx.lineTo(bx + tw - 3, by); ctx.arcTo(bx + tw, by, bx + tw, by + 3, 3);
|
||||
ctx.lineTo(bx + tw, by + 13); ctx.arcTo(bx + tw, by + 16, bx + tw - 3, by + 16, 3);
|
||||
ctx.lineTo(bx + 3, by + 16); ctx.arcTo(bx, by + 16, bx, by + 13, 3);
|
||||
ctx.lineTo(bx, by + 3); ctx.arcTo(bx, by, bx + 3, by, 3);
|
||||
ctx.fill();
|
||||
ctx.beginPath(); ctx.moveTo(x - 3, by + 16); ctx.lineTo(x, by + 20); ctx.lineTo(x + 3, by + 16); ctx.fill();
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(text, x, by + 11);
|
||||
ctx.textAlign = "start";
|
||||
}
|
||||
|
||||
ctx.fillStyle = "#ccc";
|
||||
ctx.fillStyle = "#bbb";
|
||||
ctx.font = "9px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(agent.name.split(" ")[0], x, y + 6);
|
||||
ctx.fillText(agent.name.split(" ")[0], x, y + 10);
|
||||
ctx.textAlign = "start";
|
||||
}
|
||||
|
||||
function drawSpeechBubble(ctx: CanvasRenderingContext2D, x: number, y: number, text: string) {
|
||||
ctx.font = "7px monospace";
|
||||
const tw = ctx.measureText(text).width + 12;
|
||||
const bx = x - tw / 2;
|
||||
ctx.fillStyle = "#fff";
|
||||
roundRect(ctx, bx, y - 14, tw, 16, 4);
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 3, y + 2);
|
||||
ctx.lineTo(x, y + 7);
|
||||
ctx.lineTo(x + 3, y + 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#333";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(text, x, y - 3);
|
||||
ctx.textAlign = "start";
|
||||
}
|
||||
|
||||
function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function groupByDepartment(agents: OfficeAgent[]): Map<string, OfficeAgent[]> {
|
||||
const m = new Map<string, OfficeAgent[]>();
|
||||
for (const a of agents) {
|
||||
if (!m.has(a.department)) m.set(a.department, []);
|
||||
m.get(a.department)!.push(a);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
Reference in new issue
Block a user