diff --git a/app/pixel-office/page.tsx b/app/pixel-office/page.tsx index 4a32ef7..2cd9296 100644 --- a/app/pixel-office/page.tsx +++ b/app/pixel-office/page.tsx @@ -107,6 +107,7 @@ function getGhostBorderDirection(col: number, row: number, cols: number, rows: n } const FIXED_CANVAS_ZOOM = 2.5 +const CODE_SNIPPET_LIFETIME_SEC = 5.5 let cachedOfficeState: OfficeState | null = null let cachedEditorState: EditorState | null = null @@ -157,6 +158,7 @@ export default function PixelOfficePage() { const [showIdleRank, setShowIdleRank] = useState(false) const idleRankRef = useRef | null>(null) const floatingCommentsRef = useRef>([]) + const floatingCodeRef = useRef>([]) const [floatingTick, setFloatingTick] = useState(0) const forceEditorUpdate = useCallback(() => setEditorTick(t => t + 1), []) @@ -318,6 +320,13 @@ export default function PixelOfficePage() { const containerTop = container.offsetTop const lifetime = 4.0 const items: Array<{ key: string; text: string; x: number; y: number; opacity: number }> = [] + const codeItems: Array<{ key: string; text: string; x: number; y: number; opacity: number }> = [] + const workingCharIds = new Set() + for (const a of agents) { + if (a.state !== 'working') continue + const cid = agentIdMapRef.current.get(a.agentId) + if (typeof cid === 'number') workingCharIds.add(cid) + } for (const ch of office.getCharacters()) { if (ch.photoComments.length === 0) continue const anchorX = ox + ch.x * zoom @@ -339,7 +348,28 @@ export default function PixelOfficePage() { }) } } + for (const ch of office.getCharacters()) { + if (!workingCharIds.has(ch.id)) continue + if (ch.codeSnippets.length === 0) continue + const anchorX = ox + ch.x * zoom + const anchorY = containerTop + oy + (ch.y - 10) * zoom + const totalDist = anchorY + 24 + for (let i = 0; i < ch.codeSnippets.length; i++) { + const s = ch.codeSnippets[i] + const progress = s.age / CODE_SNIPPET_LIFETIME_SEC + if (progress <= 0 || progress >= 1) continue + const alpha = progress < 0.15 ? progress / 0.15 : progress > 0.88 ? (1 - progress) / 0.12 : 1 + codeItems.push({ + key: `${ch.id}-code-${i}-${s.text}`, + text: s.text, + x: anchorX + s.x * zoom, + y: anchorY - progress * totalDist, + opacity: Math.max(0, alpha * 0.9), + }) + } + } floatingCommentsRef.current = items + floatingCodeRef.current = codeItems setFloatingTick(t => t + 1) } animationFrameIdRef.current = requestAnimationFrame(render) @@ -348,7 +378,7 @@ export default function PixelOfficePage() { return () => { if (animationFrameIdRef.current !== null) cancelAnimationFrame(animationFrameIdRef.current) } - }, [hoveredAgentId, editorTick, officeReady]) + }, [hoveredAgentId, editorTick, officeReady, agents]) // Load GitHub contribution heatmap data (real → fallback mock) useEffect(() => { @@ -1055,6 +1085,18 @@ export default function PixelOfficePage() { ))} + {/* Floating code snippets (working agents): rise to top, overlay top bar */} + {floatingCodeRef.current.map(fc => ( +
+ + {fc.text} + +
+ ))} {/* Top bar: agent tags + controls */}
{t('pixelOffice.title')} diff --git a/lib/pixel-office/engine/officeState.ts b/lib/pixel-office/engine/officeState.ts index 1316eb1..1b6100e 100644 --- a/lib/pixel-office/engine/officeState.ts +++ b/lib/pixel-office/engine/officeState.ts @@ -32,7 +32,7 @@ import { BugSystem } from '../bugs/bugSystem' import { BUG_DEFAULT_COUNT } from '../bugs/config' import type { BugEntity } from '../bugs/types' -const CODE_SNIPPET_LIFETIME = 2.5 // seconds +const CODE_SNIPPET_LIFETIME = 5.5 // seconds const CODE_SNIPPET_SPAWN_RATE = 0.6 // per second const PHOTO_COMMENT_LIFETIME = 4.0 // seconds const PHOTO_COMMENT_SPAWN_RATE = 2.5 // per second diff --git a/lib/pixel-office/engine/renderer.ts b/lib/pixel-office/engine/renderer.ts index 744458d..d9491fd 100644 --- a/lib/pixel-office/engine/renderer.ts +++ b/lib/pixel-office/engine/renderer.ts @@ -426,55 +426,8 @@ export function renderScene( }) } - // Code snippet particles floating above working characters - if (ch.codeSnippets.length > 0) { - const snippets = ch.codeSnippets - const baseX = Math.round(offsetX + ch.x * zoom) - const baseY = drawY - const snipFontSize = Math.max(12, Math.round(7 * zoom)) - drawables.push({ - zY: charZY + 0.2, - draw: (c) => { - c.save() - c.font = `${snipFontSize}px monospace` - c.textAlign = 'center' - c.textBaseline = 'bottom' - for (const s of snippets) { - const progress = s.age / 2.5 // 0→1 over lifetime - const floatY = baseY - (10 + progress * 28) * zoom - const floatX = baseX + s.x * zoom - const alpha = progress < 0.2 ? progress / 0.2 : progress > 0.7 ? (1 - progress) / 0.3 : 1 - c.globalAlpha = alpha * 0.85 - // Background pill - const tw = c.measureText(s.text).width - const px = 3 * (zoom / 3) - const py = 1.5 * (zoom / 3) - c.fillStyle = 'rgba(0,0,0,0.7)' - const rx = floatX - tw / 2 - px - const ry = floatY - snipFontSize - py - const rw = tw + px * 2 - const rh = snipFontSize + py * 2 - const cr = 3 - c.beginPath() - c.moveTo(rx + cr, ry) - c.lineTo(rx + rw - cr, ry) - c.quadraticCurveTo(rx + rw, ry, rx + rw, ry + cr) - c.lineTo(rx + rw, ry + rh - cr) - c.quadraticCurveTo(rx + rw, ry + rh, rx + rw - cr, ry + rh) - c.lineTo(rx + cr, ry + rh) - c.quadraticCurveTo(rx, ry + rh, rx, ry + rh - cr) - c.lineTo(rx, ry + cr) - c.quadraticCurveTo(rx, ry, rx + cr, ry) - c.closePath() - c.fill() - // Text - c.fillStyle = '#4ade80' - c.fillText(s.text, floatX, floatY) - } - c.restore() - }, - }) - } + // Code snippet particles are rendered as DOM overlays in app/pixel-office/page.tsx + // so they can float beyond the canvas area and pass over the top agent list. } // Sort by Y (lower = in front = drawn later)