mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
- 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)
131 lines
4.8 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|
|
}
|