From 96a5d97ff74748790c7be0c935ffc4f145442bb7 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 11 Apr 2026 08:57:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(agent-office):=20Canvas=20engine=20?= =?UTF-8?q?=E2=80=94=20SpriteSheet,=20TileMap,=20office-map=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- src/pages/agent-office/assets/office-map.json | 45 ++++++++++ src/pages/agent-office/canvas/SpriteSheet.js | 89 ++++++++++++++++++ src/pages/agent-office/canvas/TileMap.js | 90 +++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 src/pages/agent-office/assets/office-map.json create mode 100644 src/pages/agent-office/canvas/SpriteSheet.js create mode 100644 src/pages/agent-office/canvas/TileMap.js diff --git a/src/pages/agent-office/assets/office-map.json b/src/pages/agent-office/assets/office-map.json new file mode 100644 index 0000000..509563e --- /dev/null +++ b/src/pages/agent-office/assets/office-map.json @@ -0,0 +1,45 @@ +{ + "tileSize": 32, + "cols": 20, + "rows": 14, + "layers": { + "floor": [ + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] + ] + }, + "furniture": [ + {"type": "desk", "x": 2, "y": 1, "label": "Stock"}, + {"type": "desk", "x": 7, "y": 1, "label": "Music"}, + {"type": "desk", "x": 12, "y": 1, "label": "Claude"}, + {"type": "desk", "x": 17, "y": 1, "label": "(빈)"}, + {"type": "table", "x": 8, "y": 6, "w": 4, "h": 2, "label": "회의 테이블"}, + {"type": "sofa", "x": 1, "y": 10, "label": "휴게실"}, + {"type": "coffee", "x": 3, "y": 10, "label": "☕"}, + {"type": "desk", "x": 14, "y": 10, "w": 5, "h": 2, "label": "CEO"} + ], + "waypoints": { + "stock_desk": {"x": 2, "y": 2}, + "music_desk": {"x": 7, "y": 2}, + "claude_desk": {"x": 12, "y": 2}, + "meeting_table": {"x": 9, "y": 7}, + "break_room": {"x": 2, "y": 11}, + "ceo_desk": {"x": 16, "y": 11} + }, + "colors": { + "1": "#3a3a50", + "2": "#4a3a2a" + } +} diff --git a/src/pages/agent-office/canvas/SpriteSheet.js b/src/pages/agent-office/canvas/SpriteSheet.js new file mode 100644 index 0000000..1693617 --- /dev/null +++ b/src/pages/agent-office/canvas/SpriteSheet.js @@ -0,0 +1,89 @@ +const PIXEL_CHARS = { + stock: { body: '#4488cc', accent: '#cc4444', label: '주식', hair: '#332222' }, + music: { body: '#44aa88', accent: '#ffaa00', label: '음악', hair: '#443322' }, + claude: { body: '#8855cc', accent: '#cc88ff', label: 'Claude', hair: '#554466' }, +}; + +const ANIM_FRAMES = { + idle: { frames: 2, speed: 800 }, + working: { frames: 4, speed: 200 }, + waiting: { frames: 2, speed: 400 }, + break: { frames: 2, speed: 1000 }, + walk: { frames: 4, speed: 150 }, +}; + +export function drawAgent(ctx, agentId, x, y, state, frameIndex, scale = 2) { + const char = PIXEL_CHARS[agentId] || PIXEL_CHARS.claude; + const s = scale; + const anim = ANIM_FRAMES[state] || ANIM_FRAMES.idle; + const frame = frameIndex % anim.frames; + + ctx.save(); + ctx.translate(x, y); + + // Shadow + ctx.fillStyle = 'rgba(0,0,0,0.2)'; + ctx.fillRect(-4 * s, 14 * s, 8 * s, 2 * s); + + // Body + ctx.fillStyle = char.body; + ctx.fillRect(-3 * s, 2 * s, 6 * s, 8 * s); + + // Head + ctx.fillStyle = '#ffcc99'; + ctx.fillRect(-3 * s, -4 * s, 6 * s, 6 * s); + + // Hair + ctx.fillStyle = char.hair; + ctx.fillRect(-3 * s, -5 * s, 6 * s, 2 * s); + + // Eyes + ctx.fillStyle = '#222'; + const eyeOffset = state === 'break' && frame === 1 ? 0 : 1; + ctx.fillRect(-2 * s, -1 * s, 1 * s, eyeOffset * s); + ctx.fillRect(1 * s, -1 * s, 1 * s, eyeOffset * s); + + // Legs + ctx.fillStyle = '#335'; + const legSpread = state === 'walk' ? (frame % 2 === 0 ? 1 : -1) : 0; + ctx.fillRect(-2 * s, 10 * s, 2 * s, 4 * s); + ctx.fillRect(0 + legSpread * s, 10 * s, 2 * s, 4 * s); + + // Accent + ctx.fillStyle = char.accent; + if (agentId === 'stock') { + ctx.fillRect(0, 2 * s, 1 * s, 5 * s); + } else if (agentId === 'music') { + ctx.fillRect(-4 * s, -4 * s, 1 * s, 4 * s); + ctx.fillRect(3 * s, -4 * s, 1 * s, 4 * s); + ctx.fillRect(-4 * s, -5 * s, 8 * s, 1 * s); + } else if (agentId === 'claude') { + ctx.globalAlpha = 0.3 + 0.2 * Math.sin(Date.now() / 500); + ctx.fillRect(-4 * s, -6 * s, 8 * s, 1 * s); + ctx.globalAlpha = 1; + } + + // Working: typing hands + if (state === 'working') { + ctx.fillStyle = '#ffcc99'; + const handY = 6 * s + (frame % 2) * s; + ctx.fillRect(-4 * s, handY, 1 * s, 2 * s); + ctx.fillRect(3 * s, handY, 1 * s, 2 * s); + } + + // Waiting wobble + if (state === 'waiting') { + const wobble = Math.sin(Date.now() / 200) * s; + ctx.translate(wobble, 0); + } + + ctx.restore(); +} + +export function getAnimSpeed(state) { + return (ANIM_FRAMES[state] || ANIM_FRAMES.idle).speed; +} + +export function getCharLabel(agentId) { + return (PIXEL_CHARS[agentId] || {}).label || agentId; +} diff --git a/src/pages/agent-office/canvas/TileMap.js b/src/pages/agent-office/canvas/TileMap.js new file mode 100644 index 0000000..be1ae3f --- /dev/null +++ b/src/pages/agent-office/canvas/TileMap.js @@ -0,0 +1,90 @@ +const WALL_COLOR = '#2a2a3a'; +const DESK_COLOR = '#6b5b3a'; +const DESK_TOP = '#8b7b5a'; +const TABLE_COLOR = '#5a4a2a'; +const SOFA_COLOR = '#884444'; +const MONITOR_COLOR = '#224466'; +const MONITOR_SCREEN = '#44aacc'; + +export function drawTileMap(ctx, mapData, width, height) { + const { tileSize, cols, rows, layers, furniture, colors } = mapData; + const scaleX = width / (cols * tileSize); + const scaleY = height / (rows * tileSize); + const scale = Math.min(scaleX, scaleY); + + const offsetX = (width - cols * tileSize * scale) / 2; + const offsetY = (height - rows * tileSize * scale) / 2; + + ctx.save(); + ctx.translate(offsetX, offsetY); + ctx.scale(scale, scale); + + const floor = layers.floor; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const tile = floor[r][c]; + ctx.fillStyle = colors[String(tile)] || '#3a3a50'; + ctx.fillRect(c * tileSize, r * tileSize, tileSize, tileSize); + ctx.strokeStyle = 'rgba(255,255,255,0.03)'; + ctx.strokeRect(c * tileSize, r * tileSize, tileSize, tileSize); + } + } + + ctx.fillStyle = WALL_COLOR; + ctx.fillRect(0, 0, cols * tileSize, 4); + + for (const f of furniture) { + const fx = f.x * tileSize; + const fy = f.y * tileSize; + const fw = (f.w || 2) * tileSize; + const fh = (f.h || 2) * tileSize; + + if (f.type === 'desk') { + ctx.fillStyle = DESK_COLOR; + ctx.fillRect(fx, fy, fw, fh); + ctx.fillStyle = DESK_TOP; + ctx.fillRect(fx + 2, fy + 2, fw - 4, 6); + const mx = fx + fw / 2 - 8; + ctx.fillStyle = MONITOR_COLOR; + ctx.fillRect(mx, fy + 4, 16, 12); + ctx.fillStyle = MONITOR_SCREEN; + ctx.fillRect(mx + 2, fy + 6, 12, 8); + if (f.label) { + ctx.fillStyle = 'rgba(255,255,255,0.6)'; + ctx.font = '8px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(f.label, fx + fw / 2, fy + fh + 12); + } + } else if (f.type === 'table') { + ctx.fillStyle = TABLE_COLOR; + ctx.fillRect(fx, fy, fw, fh); + ctx.fillStyle = '#7a6a4a'; + ctx.fillRect(fx + 4, fy + 4, fw - 8, fh - 8); + } else if (f.type === 'sofa') { + ctx.fillStyle = SOFA_COLOR; + ctx.fillRect(fx, fy, 48, 32); + ctx.fillStyle = '#aa5555'; + ctx.fillRect(fx + 4, fy + 4, 40, 24); + } else if (f.type === 'coffee') { + ctx.fillStyle = '#664422'; + ctx.fillRect(fx + 8, fy + 8, 16, 20); + ctx.fillStyle = '#886644'; + ctx.fillRect(fx + 6, fy + 6, 20, 4); + } + } + + ctx.restore(); + return { scale, offsetX, offsetY, tileSize }; +} + +export function worldToTile(mapData, renderInfo, canvasX, canvasY) { + const { scale, offsetX, offsetY, tileSize } = renderInfo; + const wx = (canvasX - offsetX) / scale; + const wy = (canvasY - offsetY) / scale; + return { col: Math.floor(wx / tileSize), row: Math.floor(wy / tileSize), worldX: wx, worldY: wy }; +} + +export function tileToCanvas(mapData, renderInfo, col, row) { + const { scale, offsetX, offsetY, tileSize } = renderInfo; + return { x: offsetX + col * tileSize * scale + (tileSize * scale) / 2, y: offsetY + row * tileSize * scale + (tileSize * scale) / 2 }; +}