feat(pixel-office): float working code snippets above top bar

This commit is contained in:
xmanrui
2026-03-02 05:39:06 +08:00
parent 08bb876735
commit 65bdb81af3
3 changed files with 46 additions and 51 deletions
+43 -1
View File
@@ -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<Array<{ agentId: string; onlineMinutes: number; activeMinutes: number; idleMinutes: number; idlePercent: number }> | null>(null)
const floatingCommentsRef = useRef<Array<{ key: string; text: string; x: number; y: number; opacity: number }>>([])
const floatingCodeRef = useRef<Array<{ key: string; text: string; x: number; y: number; opacity: number }>>([])
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<number>()
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() {
</span>
</div>
))}
{/* Floating code snippets (working agents): rise to top, overlay top bar */}
{floatingCodeRef.current.map(fc => (
<div key={fc.key} className="absolute pointer-events-none z-40 whitespace-nowrap"
style={{ left: fc.x, top: fc.y, opacity: fc.opacity, transform: 'translateX(-50%)' }}>
<span
className="inline-block px-2 py-0.5 rounded-md text-[11px] font-mono font-semibold"
style={{ backgroundColor: 'rgba(0,0,0,0.72)', color: '#4ade80' }}
>
{fc.text}
</span>
</div>
))}
{/* Top bar: agent tags + controls */}
<div className="flex flex-wrap items-center gap-2 p-4 border-b border-[var(--border)]">
<span className="text-sm font-bold text-[var(--text)] mr-2">{t('pixelOffice.title')}</span>
+1 -1
View File
@@ -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
+2 -49
View File
@@ -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)