Files
OpenClaw-bot-review/lib/pixel-office/engine/matrixEffect.ts
T
xmanrui eebe431560 feat: implement Pixel Office Phase 2 - layout editor, persistence, sound, sub-agent visualization
- Add layout editor with floor/wall paint, erase, furniture place/move/rotate/delete tools
- Add editor state management (undo/redo, selection, drag, ghost preview)
- Add EditorToolbar and EditActionBar UI components
- Add layout persistence API (GET/POST to ~/.openclaw/pixel-office/layout.json)
- Add notification sound system (Web Audio API ascending chime on agent completion)
- Add sub-agent visualization via session JSONL parsing
- Fix agent activity API to read from agents.list config and correct session paths
- Add agent name labels above character sprites in canvas renderer
- Add i18n translations for editor features (zh/en)
2026-02-25 01:39:44 +08:00

131 lines
4.8 KiB
TypeScript

import type { Character, SpriteData } from '../types'
import { MATRIX_EFFECT_DURATION } from '../types'
import {
MATRIX_TRAIL_LENGTH,
MATRIX_SPRITE_COLS,
MATRIX_SPRITE_ROWS,
MATRIX_FLICKER_FPS,
MATRIX_FLICKER_VISIBILITY_THRESHOLD,
MATRIX_COLUMN_STAGGER_RANGE,
MATRIX_HEAD_COLOR,
MATRIX_TRAIL_OVERLAY_ALPHA,
MATRIX_TRAIL_EMPTY_ALPHA,
MATRIX_TRAIL_MID_THRESHOLD,
MATRIX_TRAIL_DIM_THRESHOLD,
} from '../constants'
/** Hash-based flicker: ~70% visible for shimmer effect */
function flickerVisible(col: number, row: number, time: number): boolean {
const t = Math.floor(time * MATRIX_FLICKER_FPS)
const hash = ((col * 7 + row * 13 + t * 31) & 0xff)
return hash < MATRIX_FLICKER_VISIBILITY_THRESHOLD
}
function generateSeeds(): number[] {
const seeds: number[] = []
for (let i = 0; i < MATRIX_SPRITE_COLS; i++) {
seeds.push(Math.random())
}
return seeds
}
export { generateSeeds as matrixEffectSeeds }
/**
* Render a character with a Matrix-style digital rain spawn/despawn effect.
* Per-pixel rendering: each column sweeps top-to-bottom with a bright head and fading green trail.
*/
export function renderMatrixEffect(
ctx: CanvasRenderingContext2D,
ch: Character,
spriteData: SpriteData,
drawX: number,
drawY: number,
zoom: number,
): void {
const progress = ch.matrixEffectTimer / MATRIX_EFFECT_DURATION
const isSpawn = ch.matrixEffect === 'spawn'
const time = ch.matrixEffectTimer
const totalSweep = MATRIX_SPRITE_ROWS + MATRIX_TRAIL_LENGTH
for (let col = 0; col < MATRIX_SPRITE_COLS; col++) {
// Stagger: each column starts at a slightly different time
const stagger = (ch.matrixEffectSeeds[col] ?? 0) * MATRIX_COLUMN_STAGGER_RANGE
const colProgress = Math.max(0, Math.min(1, (progress - stagger) / (1 - MATRIX_COLUMN_STAGGER_RANGE)))
const headRow = colProgress * totalSweep
for (let row = 0; row < MATRIX_SPRITE_ROWS; row++) {
const pixel = spriteData[row]?.[col]
const hasPixel = pixel && pixel !== ''
const distFromHead = headRow - row
const px = drawX + col * zoom
const py = drawY + row * zoom
if (isSpawn) {
// Spawn: head sweeps down revealing character pixels
if (distFromHead < 0) {
// Above head: invisible
continue
} else if (distFromHead < 1) {
// Head pixel: bright white-green
ctx.fillStyle = MATRIX_HEAD_COLOR
ctx.fillRect(px, py, zoom, zoom)
} else if (distFromHead < MATRIX_TRAIL_LENGTH) {
// Trail zone: show character pixel with green overlay, or just green if no pixel
const trailPos = distFromHead / MATRIX_TRAIL_LENGTH
if (hasPixel) {
// Draw original pixel
ctx.fillStyle = pixel
ctx.fillRect(px, py, zoom, zoom)
// Green overlay that fades as trail progresses
const greenAlpha = (1 - trailPos) * MATRIX_TRAIL_OVERLAY_ALPHA
if (flickerVisible(col, row, time)) {
ctx.fillStyle = `rgba(0, 255, 65, ${greenAlpha})`
ctx.fillRect(px, py, zoom, zoom)
}
} else {
// No character pixel: fading green trail
if (flickerVisible(col, row, time)) {
const alpha = (1 - trailPos) * MATRIX_TRAIL_EMPTY_ALPHA
ctx.fillStyle = trailPos < MATRIX_TRAIL_MID_THRESHOLD ? `rgba(0, 255, 65, ${alpha})`
: trailPos < MATRIX_TRAIL_DIM_THRESHOLD ? `rgba(0, 170, 40, ${alpha})`
: `rgba(0, 85, 20, ${alpha})`
ctx.fillRect(px, py, zoom, zoom)
}
}
} else {
// Below trail: normal character pixel
if (hasPixel) {
ctx.fillStyle = pixel
ctx.fillRect(px, py, zoom, zoom)
}
}
} else {
// Despawn: head sweeps down consuming character pixels
if (distFromHead < 0) {
// Above head: normal character pixel (not yet consumed)
if (hasPixel) {
ctx.fillStyle = pixel
ctx.fillRect(px, py, zoom, zoom)
}
} else if (distFromHead < 1) {
// Head pixel: bright white-green
ctx.fillStyle = MATRIX_HEAD_COLOR
ctx.fillRect(px, py, zoom, zoom)
} else if (distFromHead < MATRIX_TRAIL_LENGTH) {
// Trail zone: fading green
if (flickerVisible(col, row, time)) {
const trailPos = distFromHead / MATRIX_TRAIL_LENGTH
const alpha = (1 - trailPos) * MATRIX_TRAIL_EMPTY_ALPHA
ctx.fillStyle = trailPos < MATRIX_TRAIL_MID_THRESHOLD ? `rgba(0, 255, 65, ${alpha})`
: trailPos < MATRIX_TRAIL_DIM_THRESHOLD ? `rgba(0, 170, 40, ${alpha})`
: `rgba(0, 85, 20, ${alpha})`
ctx.fillRect(px, py, zoom, zoom)
}
}
// Below trail: nothing (consumed)
}
}
}
}