feat: add orchestration room map (procedural + external asset support)
map.js provides: - MAP_POSITIONS: pixel coords for conductor, secretary, 5 stations, door, meeting area — the single source of truth for character placement - loadMapAsset(cb): tries to load map.png; falls back to procedural drawing - drawMap(ctx): uses loaded image or procedural fallback transparently To replace the procedural map with a hand-crafted asset: drop map.png (320×320px) next to index.html — no code changes required. map-preview.html: standalone preview showing the room + all character positions with emoji markers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bfabdea8b7
commit
932a4149e5
2 changed files with 302 additions and 0 deletions
98
pkg/miniapp/static/map-preview.html
Normal file
98
pkg/miniapp/static/map-preview.html
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Orchestration Room — Preview</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Silkscreen&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #060810;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
/* 2× for retina preview */
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
border: 1px solid #1a1f2e;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
font-family: 'Silkscreen', monospace;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #3a4259;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend span { color: #64748b; }
|
||||||
|
</style>
|
||||||
|
<script src="map.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="c" width="320" height="320"></canvas>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<span>👑 conductor</span>
|
||||||
|
<span>👩💼 secretary</span>
|
||||||
|
<span>🔍 scout</span>
|
||||||
|
<span>📊 analyst</span>
|
||||||
|
<span>💻 coder</span>
|
||||||
|
<span>🔧 worker</span>
|
||||||
|
<span>🎯 coordinator</span>
|
||||||
|
<span>🚪 door</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var canvas = document.getElementById('c');
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
var EMOJIS = {
|
||||||
|
conductor: '👑',
|
||||||
|
secretary: '👩💼',
|
||||||
|
meeting: null,
|
||||||
|
stations: ['🔍', '📊', '💻', '🔧', '🎯'],
|
||||||
|
};
|
||||||
|
|
||||||
|
function drawCharacters() {
|
||||||
|
ctx.font = '20px serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
|
// permanent characters
|
||||||
|
ctx.fillText(EMOJIS.conductor, MAP_POSITIONS.conductor.x, MAP_POSITIONS.conductor.y);
|
||||||
|
ctx.fillText(EMOJIS.secretary, MAP_POSITIONS.secretary.x, MAP_POSITIONS.secretary.y);
|
||||||
|
|
||||||
|
// agents at their stations
|
||||||
|
MAP_POSITIONS.stations.forEach(function(pos, i) {
|
||||||
|
ctx.fillText(EMOJIS.stations[i], pos.x, pos.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
// door marker
|
||||||
|
ctx.font = '14px serif';
|
||||||
|
ctx.fillText('🚪', MAP_POSITIONS.door.x, MAP_POSITIONS.door.y - 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load map.png if it exists, otherwise use procedural fallback
|
||||||
|
loadMapAsset(function() {
|
||||||
|
drawMap(ctx);
|
||||||
|
drawCharacters();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
204
pkg/miniapp/static/map.js
Normal file
204
pkg/miniapp/static/map.js
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
// map.js — Orchestration Room
|
||||||
|
//
|
||||||
|
// External asset: drop map.png (320×320px) next to index.html to replace
|
||||||
|
// the procedural fallback. Character positions (MAP_POSITIONS) are defined
|
||||||
|
// in canvas-pixel coordinates and remain valid regardless of which rendering
|
||||||
|
// path is used — just make sure your map.png matches them.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// loadMapAsset(function() { drawMap(ctx); }); // call once on init
|
||||||
|
// drawMap(ctx); // call each frame
|
||||||
|
|
||||||
|
// ─── Character home positions (px, canvas 320×320) ─────────────────────────
|
||||||
|
//
|
||||||
|
// ┌──────────────────────────────┐
|
||||||
|
// │ [conductor desk] │ y ≈ 20–50
|
||||||
|
// │ 👑(160,58) 👩💼(108,58) │
|
||||||
|
// │ [carpet] │
|
||||||
|
// │ [WS1] [WS2] [WS3] │ y ≈ 80–100
|
||||||
|
// │ 🔍40 💻144 📊248 │ y = 106
|
||||||
|
// │ [meeting area] │ y ≈ 130–192
|
||||||
|
// │ [WS4] [WS5] │ y ≈ 200–220
|
||||||
|
// │ 🔧40 🎯144 │ y = 222
|
||||||
|
// │ 🚪(160,308) │ door
|
||||||
|
// └──────────────────────────────┘
|
||||||
|
|
||||||
|
var MAP_POSITIONS = {
|
||||||
|
door: { x: 160, y: 314 }, // entry / exit point
|
||||||
|
conductor: { x: 160, y: 58 },
|
||||||
|
secretary: { x: 108, y: 58 },
|
||||||
|
meeting: { x: 160, y: 161 }, // neutral zone for conversations
|
||||||
|
stations: [
|
||||||
|
{ x: 40, y: 106 }, // S0 scout
|
||||||
|
{ x: 144, y: 106 }, // S1 analyst
|
||||||
|
{ x: 248, y: 106 }, // S2 coder
|
||||||
|
{ x: 40, y: 222 }, // S3 worker
|
||||||
|
{ x: 144, y: 222 }, // S4 coordinator
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Asset loading ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
var _mapImage = null;
|
||||||
|
|
||||||
|
// Call once before first draw. cb() is invoked when ready (image or fallback).
|
||||||
|
function loadMapAsset(cb) {
|
||||||
|
var img = new Image();
|
||||||
|
img.onload = function() { _mapImage = img; cb(); };
|
||||||
|
img.onerror = function() { cb(); }; // no map.png → use fallback
|
||||||
|
img.src = './map.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public draw entry point ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function drawMap(ctx) {
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
if (_mapImage) {
|
||||||
|
ctx.drawImage(_mapImage, 0, 0, 320, 320);
|
||||||
|
} else {
|
||||||
|
_drawMapFallback(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Procedural fallback ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
var _C = {
|
||||||
|
wallDark: '#0c1018',
|
||||||
|
wallHighlight: '#252d3f',
|
||||||
|
floorA: '#171b2c',
|
||||||
|
floorB: '#1b2033',
|
||||||
|
carpetBase: '#1a2050',
|
||||||
|
carpetBorder: '#2a3480',
|
||||||
|
deskBack: '#2c3e6b',
|
||||||
|
deskTop: '#3a50a0',
|
||||||
|
deskEdge: '#4a6ac0',
|
||||||
|
deskShadow: '#1a2448',
|
||||||
|
monitorFrame: '#070b14',
|
||||||
|
monitorBlue: '#1040a0',
|
||||||
|
monitorGlow: '#4488ff',
|
||||||
|
wsBase: '#162818',
|
||||||
|
wsTop: '#1e3822',
|
||||||
|
wsEdge: '#2a5030',
|
||||||
|
termGlow: '#00dd55',
|
||||||
|
rugFill: '#1c2248',
|
||||||
|
rugBorder: '#283070',
|
||||||
|
doorMid: '#8a5818',
|
||||||
|
doorLight: '#a06820',
|
||||||
|
doorGold: '#c8940a',
|
||||||
|
};
|
||||||
|
|
||||||
|
function _r(ctx, color, x, y, w, h, alpha) {
|
||||||
|
ctx.globalAlpha = alpha === undefined ? 1 : alpha;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _b(ctx, color, x, y, w, h) {
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _dot(ctx, color, x, y) {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(x, y, 2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _workstation(ctx, x, y) {
|
||||||
|
_r(ctx, _C.wsBase, x, y, 48, 20);
|
||||||
|
_r(ctx, _C.wsTop, x, y, 48, 8);
|
||||||
|
_r(ctx, _C.wsEdge, x, y, 2, 20);
|
||||||
|
_r(ctx, _C.wsEdge, x+46, y, 2, 20);
|
||||||
|
_r(ctx, _C.wsEdge, x, y, 48, 2);
|
||||||
|
// terminal screen
|
||||||
|
_r(ctx, _C.monitorFrame, x+16, y+2, 16, 12);
|
||||||
|
_r(ctx, '#041008', x+17, y+3, 14, 10);
|
||||||
|
_r(ctx, '#003315', x+18, y+4, 12, 8);
|
||||||
|
_r(ctx, _C.termGlow, x+20, y+6, 8, 3);
|
||||||
|
_dot(ctx, '#00ff88', x+22, y+6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _drawMapFallback(ctx) {
|
||||||
|
var T = 16;
|
||||||
|
|
||||||
|
// floor tiles
|
||||||
|
for (var ty = 0; ty < 20; ty++) {
|
||||||
|
for (var tx = 0; tx < 20; tx++) {
|
||||||
|
ctx.fillStyle = (tx + ty) % 2 === 0 ? _C.floorA : _C.floorB;
|
||||||
|
ctx.fillRect(tx * T, ty * T, T, T);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// conductor carpet
|
||||||
|
_r(ctx, _C.carpetBase, 16, 16, 288, 50);
|
||||||
|
_b(ctx, _C.carpetBorder, 18, 18, 284, 46);
|
||||||
|
|
||||||
|
// conductor desk
|
||||||
|
_r(ctx, _C.deskBack, 96, 20, 128, 30);
|
||||||
|
_r(ctx, _C.deskTop, 96, 20, 128, 12);
|
||||||
|
_r(ctx, _C.deskEdge, 96, 20, 128, 2);
|
||||||
|
_r(ctx, _C.deskEdge, 96, 20, 2, 30);
|
||||||
|
_r(ctx, _C.deskEdge, 222, 20, 2, 30);
|
||||||
|
_r(ctx, _C.deskShadow,96,48, 128, 4);
|
||||||
|
// monitor
|
||||||
|
_r(ctx, _C.monitorFrame, 138, 22, 44, 14);
|
||||||
|
_r(ctx, _C.monitorBlue, 140, 23, 40, 12);
|
||||||
|
_r(ctx, _C.monitorGlow, 156, 26, 8, 6);
|
||||||
|
_r(ctx, '#6699ff', 158, 27, 4, 3);
|
||||||
|
|
||||||
|
// workstations
|
||||||
|
_workstation(ctx, 16, 80); // S0
|
||||||
|
_workstation(ctx, 128, 80); // S1 (x+24 = 152 ≈ 144 center)
|
||||||
|
_workstation(ctx, 224, 80); // S2
|
||||||
|
_workstation(ctx, 16, 200); // S3
|
||||||
|
_workstation(ctx, 128, 200); // S4
|
||||||
|
|
||||||
|
// meeting rug
|
||||||
|
_r(ctx, _C.rugFill, 64, 130, 192, 62, 0.55);
|
||||||
|
_b(ctx, _C.rugBorder, 66, 132, 188, 58);
|
||||||
|
_b(ctx, '#202860', 70, 136, 180, 50);
|
||||||
|
|
||||||
|
// bulletin board (left wall)
|
||||||
|
_r(ctx, '#2c1a06', 18, 148, 36, 44);
|
||||||
|
_r(ctx, '#3a2508', 20, 150, 32, 40);
|
||||||
|
_r(ctx, '#cc9900', 22, 153, 12, 8);
|
||||||
|
_r(ctx, '#dd8800', 22, 164, 10, 6);
|
||||||
|
_r(ctx, '#bb7700', 34, 155, 13, 8);
|
||||||
|
_r(ctx, '#ccaa00', 33, 165, 11, 6);
|
||||||
|
_dot(ctx, '#ff4444', 28, 153);
|
||||||
|
_dot(ctx, '#44aaff', 41, 158);
|
||||||
|
_dot(ctx, '#44ff88', 27, 165);
|
||||||
|
|
||||||
|
// server rack (right wall)
|
||||||
|
_r(ctx, '#111122', 285, 80, 18, 112);
|
||||||
|
_r(ctx, '#181830', 287, 82, 14, 108);
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
var ry = 85 + i * 10;
|
||||||
|
_r(ctx, '#0a0a12', 288, ry, 12, 8);
|
||||||
|
var lc = ['#00ff44','#0044ff','#ff3300','#111111'][i % 4];
|
||||||
|
_r(ctx, lc, 296, ry + 2, 3, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// walls (drawn last to cover any overruns)
|
||||||
|
_r(ctx, _C.wallDark, 0, 0, 320, 16);
|
||||||
|
_r(ctx, _C.wallHighlight, 0, 14, 320, 2);
|
||||||
|
_r(ctx, _C.wallDark, 0, 0, 16, 320);
|
||||||
|
_r(ctx, _C.wallHighlight,14, 0, 2, 320);
|
||||||
|
_r(ctx, _C.wallDark, 304, 0, 16, 320);
|
||||||
|
_r(ctx, _C.wallHighlight,304, 0, 2, 320);
|
||||||
|
_r(ctx, _C.wallDark, 0, 304, 144, 16);
|
||||||
|
_r(ctx, _C.wallDark, 176, 304, 144, 16);
|
||||||
|
_r(ctx, _C.wallHighlight, 0, 304, 144, 2);
|
||||||
|
_r(ctx, _C.wallHighlight,176, 304, 144, 2);
|
||||||
|
|
||||||
|
// door
|
||||||
|
_r(ctx, '#0a0808', 144, 292, 32, 12); // outside (dark)
|
||||||
|
_r(ctx, _C.doorMid, 144, 280, 32, 24);
|
||||||
|
_r(ctx, _C.doorLight, 144, 280, 32, 3);
|
||||||
|
_r(ctx, _C.doorLight, 144, 280, 3, 24);
|
||||||
|
_r(ctx, _C.doorLight, 173, 280, 3, 24);
|
||||||
|
_r(ctx, '#4a2408', 146, 284, 12, 16); // door panels
|
||||||
|
_r(ctx, '#4a2408', 162, 284, 12, 16);
|
||||||
|
_r(ctx, _C.doorGold, 170, 291, 5, 5); // handle
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue