@mmj-map/palette 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -2
- package/dist/args.js +62 -0
- package/dist/cli.js +69 -0
- package/dist/derive.js +125 -0
- package/dist/mcp.js +93 -0
- package/dist/oklab.js +95 -0
- package/dist/server.js +175 -0
- package/dist/style.js +35 -0
- package/package.json +55 -3
- package/server.json +21 -0
- package/styles/modern-candy.json +950 -0
- package/styles/modern-dark.json +949 -0
- package/styles/modern-ink.json +950 -0
- package/styles/modern-light.json +950 -0
- package/styles/modern-neon.json +950 -0
- package/styles/modern-sand.json +950 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 meta-taro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,120 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mmj-map/palette
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**導入者が自分の配色を完全に作るための道具**。指し値(4〜5 色)から、
|
|
4
|
+
24 役割の配色、またはそのまま配れるスタイル JSON を作ります。
|
|
5
|
+
|
|
6
|
+
> **ここが吐いたものを `styles/` へ置かないこと。**
|
|
7
|
+
> MMJ が配る 6 枚は手書きの作品のままです(D-002)。
|
|
8
|
+
> この道具は**導入者のためのもの**として切り出してあります(D-021)。
|
|
9
|
+
> 出力には `mmj:generated: true` が入り、手書きと見分けが付きます。
|
|
10
|
+
|
|
11
|
+
## 使う
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 24 役割の配色(<mmj-map palette-url> にそのまま渡せる)
|
|
15
|
+
pnpm palette -- --land=#f7f9fb --water=#bfd7e8 --ink=#16202b --accent=#0a5fff
|
|
16
|
+
|
|
17
|
+
# そのまま配れるスタイル
|
|
18
|
+
pnpm palette -- --land=#f7f9fb --water=#bfd7e8 --ink=#16202b --accent=#0a5fff \
|
|
19
|
+
--style --base=modern-light --name="Acme Maps" --out=acme.json
|
|
20
|
+
|
|
21
|
+
# 出したあとに確かめる
|
|
22
|
+
pnpm style:check -- acme.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| 引数 | 意味 |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `--land` | 陸・背景の色(**必須**) |
|
|
28
|
+
| `--water` | 水域の色(**必須**)。陸の系列から独立させる |
|
|
29
|
+
| `--ink` | 文字の色(**必須**)。ラベルの一番濃い側 |
|
|
30
|
+
| `--accent` | テーマカラー(**必須**)。高速道路と駅の縁に乗る |
|
|
31
|
+
| `--green` | 公園・森。省くと陸から導く |
|
|
32
|
+
| `--set=役割=色` | 役割を手で上書きする。**最後に勝つ**。何度でも書ける |
|
|
33
|
+
| `--style` | 配色ではなくスタイルごと作る |
|
|
34
|
+
| `--base` | 土台のスタイル名。既定 `modern-light`(`--style` のとき) |
|
|
35
|
+
| `--name` | 作るスタイルの名前。既定 `Custom` |
|
|
36
|
+
| `--out` | 書き出し先。**無ければ標準出力** |
|
|
37
|
+
|
|
38
|
+
## 何が導かれるか
|
|
39
|
+
|
|
40
|
+
**指し値はそのまま出ます**(作り変えません)。
|
|
41
|
+
|
|
42
|
+
| 役割 | どうなるか |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `background` `earth` `station-fill` `halo` | `--land` そのもの |
|
|
45
|
+
| `water` | `--water` そのもの |
|
|
46
|
+
| `label-city` | `--ink` そのもの |
|
|
47
|
+
| `highway` `station` | `--accent` そのもの |
|
|
48
|
+
| `landcover` `built` `paved` `buildings` `path` `road-*` `rail` `boundary` `label-station` `label-neighbourhood` | 地色から文字色へ向かって濃くなる。**細いものほど地色に近い** |
|
|
49
|
+
| `waterway` `label-water` | 水から文字色へ向かって濃くなる |
|
|
50
|
+
| `highway-casing` | accent を文字色側へ寄せた色 |
|
|
51
|
+
| `green` | `--green`、無ければ陸から導く |
|
|
52
|
+
|
|
53
|
+
混ぜるのは **Oklab**(sRGB のまま混ぜると階調が詰まり、色がくすむ)。**依存は足していません**(式だけ)。
|
|
54
|
+
|
|
55
|
+
### 好みで変わるところは式にしていない
|
|
56
|
+
|
|
57
|
+
明るい土台で**道路を白抜き**にしたい(地色より明るくする)ことがありますが、
|
|
58
|
+
ここでは地色から文字色へ向かって濃くしています。白抜きにしたいときは上書きしてください。
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm palette -- ... --set=road-minor=#ffffff --set=road-medium=#ffffff --set=road-major=#ffffff
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## MCP の口
|
|
65
|
+
|
|
66
|
+
エージェントから直接作れます。
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pnpm palette:mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Claude Code への登録(`.mcp.json` / `claude mcp add`):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"mmj-palette": {
|
|
78
|
+
"command": "pnpm",
|
|
79
|
+
"args": ["palette:mcp"],
|
|
80
|
+
"cwd": "<このリポジトリのパス>"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| 道具 | 何をするか |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `mmj_list_roles` | 24 役割と、それぞれが地図の何を変えるかを返す |
|
|
89
|
+
| `mmj_list_bases` | 土台にできるスタイル名を返す |
|
|
90
|
+
| `mmj_derive_palette` | 指し値から 24 役割の配色を作る |
|
|
91
|
+
| `mmj_build_style` | 指し値からスタイル JSON を作る |
|
|
92
|
+
|
|
93
|
+
**SDK は入れていません**。MCP の stdio は JSON-RPC 2.0 を改行区切りで流すだけで、
|
|
94
|
+
ここで要るのは `initialize` / `tools/list` / `tools/call` の 3 つです。
|
|
95
|
+
公式 SDK(`@modelcontextprotocol/sdk` 1.30.0・MIT)は express / hono / cors / jose /
|
|
96
|
+
express-rate-limit まで引いてきて **99 パッケージ増えます**(実測・2026-09-21)。
|
|
97
|
+
HTTP と SSE の輸送のためのもので、**stdio 1 本には釣り合いません**(baseline §1 / §12)。
|
|
98
|
+
|
|
99
|
+
代わりに、**本物のプロセスを起動して本物の JSON-RPC を流すテスト**があります
|
|
100
|
+
(`test/mcp.test.ts`)。仕様が動いたらそこが落ちます。
|
|
101
|
+
|
|
102
|
+
## 使う側
|
|
103
|
+
|
|
104
|
+
```html
|
|
105
|
+
<!-- 配色だけ渡す -->
|
|
106
|
+
<mmj-map tiles="..." style-url="/styles/modern-light.json" palette-url="./acme-palette.json"></mmj-map>
|
|
107
|
+
|
|
108
|
+
<!-- スタイルごと持つ -->
|
|
109
|
+
<mmj-map tiles="..." style-url="./acme.json"></mmj-map>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
スタイルごと作った場合、`metadata["mmj:anchors"].accent` を部品が読むので、
|
|
113
|
+
**`accent` 属性を書かなくても目印・まとまり・自前 POI の色が揃います**。
|
|
114
|
+
|
|
115
|
+
## 出したものの扱い
|
|
116
|
+
|
|
117
|
+
- **あなたのものです**。手で直して構いません(そのための形にしてあります)
|
|
118
|
+
- `__TILES_URL__` は残ります。**焼き込むと配信先を変えられなくなる**ため
|
|
119
|
+
- `© OpenStreetMap contributors` の帰属表示は残ります。**消して配らないでください**(ODbL)
|
|
120
|
+
- `mmj:generated: true` が入ります。**外さないでください**(手書きの作品と混ざります)
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ROLE_ORDER } from "./derive.js";
|
|
2
|
+
const ANCHOR_KEYS = ["land", "water", "ink", "accent"];
|
|
3
|
+
const KNOWN = new Set(["land", "water", "ink", "accent", "green", "base", "name", "out", "set"]);
|
|
4
|
+
/** `--base` を付けなかったときの土台 */
|
|
5
|
+
const DEFAULT_BASE = "modern-light";
|
|
6
|
+
/** `--name` を付けなかったときの名前 */
|
|
7
|
+
const DEFAULT_NAME = "Custom";
|
|
8
|
+
/**
|
|
9
|
+
* @param argv `process.argv.slice(2)` 相当
|
|
10
|
+
*/
|
|
11
|
+
export function parseArgs(argv) {
|
|
12
|
+
const values = {};
|
|
13
|
+
const overrides = {};
|
|
14
|
+
let mode = "palette";
|
|
15
|
+
for (const arg of argv) {
|
|
16
|
+
// `pnpm palette -- --land=...` の `--` はそのまま渡ってくる。**読み飛ばす**
|
|
17
|
+
if (arg === "--")
|
|
18
|
+
continue;
|
|
19
|
+
if (arg === "--style") {
|
|
20
|
+
mode = "style";
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const match = /^--([A-Za-z-]+)=(.*)$/.exec(arg);
|
|
24
|
+
if (!match)
|
|
25
|
+
throw new Error(`読めない引数です: ${arg}`);
|
|
26
|
+
const [, key = "", value = ""] = match;
|
|
27
|
+
if (!KNOWN.has(key)) {
|
|
28
|
+
throw new Error(`知らない引数です: --${key}(使えるのは ${[...KNOWN].map((k) => `--${k}`).join(" ")})`);
|
|
29
|
+
}
|
|
30
|
+
if (key === "set") {
|
|
31
|
+
const at = value.indexOf("=");
|
|
32
|
+
if (at <= 0)
|
|
33
|
+
throw new Error(`--set は 役割=色 の形で書きます: --set=${value}`);
|
|
34
|
+
const role = value.slice(0, at);
|
|
35
|
+
if (!ROLE_ORDER.includes(role)) {
|
|
36
|
+
throw new Error(`知らない役割です: ${role}(使えるのは ${ROLE_ORDER.join(" ")})`);
|
|
37
|
+
}
|
|
38
|
+
overrides[role] = value.slice(at + 1);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
values[key] = value;
|
|
42
|
+
}
|
|
43
|
+
const missing = ANCHOR_KEYS.filter((key) => values[key] === undefined || values[key] === "");
|
|
44
|
+
if (missing.length > 0) {
|
|
45
|
+
// 足りない色を勝手に作らない(baseline §11)
|
|
46
|
+
throw new Error(`指し値が足りません: ${missing.map((k) => `--${k}`).join(" ")}`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
anchors: {
|
|
50
|
+
land: values["land"],
|
|
51
|
+
water: values["water"],
|
|
52
|
+
ink: values["ink"],
|
|
53
|
+
accent: values["accent"],
|
|
54
|
+
...(values["green"] === undefined ? {} : { green: values["green"] }),
|
|
55
|
+
},
|
|
56
|
+
overrides,
|
|
57
|
+
mode,
|
|
58
|
+
base: values["base"] ?? DEFAULT_BASE,
|
|
59
|
+
name: values["name"] ?? DEFAULT_NAME,
|
|
60
|
+
out: values["out"] ?? null,
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 指し値から配色を作る。
|
|
3
|
+
*
|
|
4
|
+
* pnpm palette -- --land=#f7f9fb --water=#bfd7e8 --ink=#16202b --accent=#0a5fff
|
|
5
|
+
* pnpm palette -- ... --style --base=modern-dark --name="Acme Maps" --out=acme.json
|
|
6
|
+
* pnpm palette -- ... --set=road-minor=#ffffff 役割を手で上書きする
|
|
7
|
+
*
|
|
8
|
+
* 既定は **24 役割の JSON**(`<mmj-map palette-url>` にそのまま渡せる)。
|
|
9
|
+
* `--style` を付けると**スタイルごと**作る(`<mmj-map style-url>` / 自分の配布物へ同梱)。
|
|
10
|
+
*
|
|
11
|
+
* **出したものは導入者のもの。**`styles/` へ置かないこと(D-002 / D-021)。
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { parseArgs } from "./args.js";
|
|
17
|
+
import { deriveRoles } from "./derive.js";
|
|
18
|
+
import { buildStyle } from "./style.js";
|
|
19
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
20
|
+
/**
|
|
21
|
+
* `--out` の基準にする場所。
|
|
22
|
+
*
|
|
23
|
+
* **`process.cwd()` は使えない。** `pnpm --filter` 越しに起動されると
|
|
24
|
+
* パッケージのディレクトリになり、リポジトリのルートで打った相対パスが
|
|
25
|
+
* `tools/palette/` の中へ落ちる(実際に落ちた)。
|
|
26
|
+
* pnpm は打った場所を `INIT_CWD` で教えてくれる。
|
|
27
|
+
*/
|
|
28
|
+
const userDir = process.env["INIT_CWD"] ?? process.cwd();
|
|
29
|
+
function main(argv) {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = parseArgs(argv);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
36
|
+
console.error("");
|
|
37
|
+
console.error("例: pnpm palette -- --land=#f7f9fb --water=#bfd7e8 --ink=#16202b --accent=#0a5fff");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
const roles = deriveRoles(parsed.anchors, parsed.overrides);
|
|
41
|
+
let output = roles;
|
|
42
|
+
if (parsed.mode === "style") {
|
|
43
|
+
// 名前を経路として扱わせない(§21)
|
|
44
|
+
if (!/^[A-Za-z0-9._-]+$/.test(parsed.base)) {
|
|
45
|
+
throw new Error(`土台の名前が不正です: ${parsed.base}`);
|
|
46
|
+
}
|
|
47
|
+
const base = JSON.parse(readFileSync(resolve(repoRoot, "styles", `${parsed.base}.json`), "utf8"));
|
|
48
|
+
output = buildStyle(base, roles, { name: parsed.name, anchors: parsed.anchors });
|
|
49
|
+
}
|
|
50
|
+
const text = `${JSON.stringify(output, null, 2)}\n`;
|
|
51
|
+
if (parsed.out === null) {
|
|
52
|
+
process.stdout.write(text);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
writeFileSync(resolve(userDir, parsed.out), text, "utf8");
|
|
56
|
+
// 何を書いたかを出す。**黙って終わると、書けたのかどうか分からない**
|
|
57
|
+
console.error(`書きました: ${parsed.out}(${parsed.mode === "style" ? "スタイル" : "配色 24 役割"})`);
|
|
58
|
+
if (parsed.mode === "style")
|
|
59
|
+
console.error(`確かめる: pnpm style:check -- ${parsed.out}`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
process.exitCode = main(process.argv.slice(2));
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// 握り潰さない(§8)。色が読めない等はここに出る
|
|
67
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
package/dist/derive.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 指し値(アンカー)から 26 の役割を作る。**ここは純粋関数。**
|
|
3
|
+
*
|
|
4
|
+
* **これは D-002 が採らなかった「配色をアルゴリズムで振る」そのものです。**
|
|
5
|
+
* 人が範囲を決めて認めました(D-021・2026-09-21):
|
|
6
|
+
*
|
|
7
|
+
* 「独自で完全につくれるなら OK です」
|
|
8
|
+
*
|
|
9
|
+
* 認められた範囲は **導入者のための道具** であって、MMJ が配る `styles/*.json` の 6 枚は
|
|
10
|
+
* 手書きのまま(D-002 は生きている)。ここが吐いたものを `styles/` へ置かないこと。
|
|
11
|
+
*
|
|
12
|
+
* 出したものは**手で直される前提**なので、返すのは完成品ではなく
|
|
13
|
+
* 「役割 → 色」の表。どの行を直せば何が変わるかが分かる形にしてある。
|
|
14
|
+
*/
|
|
15
|
+
import { mix, parseHex, toHex } from "./oklab.js";
|
|
16
|
+
/**
|
|
17
|
+
* 役割の並び。**`packages/elements/src/palette.js` の `ROLE_LAYERS` と一致していること。**
|
|
18
|
+
* 一致はテストが見ている(片方だけ増やすと落ちる)。
|
|
19
|
+
*/
|
|
20
|
+
export const ROLE_ORDER = [
|
|
21
|
+
"background",
|
|
22
|
+
"earth",
|
|
23
|
+
"landcover",
|
|
24
|
+
"green",
|
|
25
|
+
"built",
|
|
26
|
+
"paved",
|
|
27
|
+
"water",
|
|
28
|
+
"waterway",
|
|
29
|
+
"buildings",
|
|
30
|
+
"path",
|
|
31
|
+
"road-minor",
|
|
32
|
+
"road-medium",
|
|
33
|
+
"road-major",
|
|
34
|
+
"highway-casing",
|
|
35
|
+
"highway",
|
|
36
|
+
"rail",
|
|
37
|
+
"rail-hatch",
|
|
38
|
+
"boundary",
|
|
39
|
+
"station",
|
|
40
|
+
"station-fill",
|
|
41
|
+
"label-city",
|
|
42
|
+
"label-station",
|
|
43
|
+
"label-neighbourhood",
|
|
44
|
+
"label-water",
|
|
45
|
+
"label-poi",
|
|
46
|
+
"halo",
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* 地色から文字色へ向かう道のり。**細いものほど地色に近い。**
|
|
50
|
+
*
|
|
51
|
+
* 明暗どちらの土台でも同じ式で使える(`land → ink` へ混ぜるだけなので、
|
|
52
|
+
* 暗い土台では明るく、明るい土台では暗くなる)。**式を 2 つ持たない。**
|
|
53
|
+
*
|
|
54
|
+
* 白抜きの道(明るい土台で道路だけ `#fff`)にしたいときは、
|
|
55
|
+
* 出したあとに `road-*` を手で直す。**そこは好みなので式にしない。**
|
|
56
|
+
*/
|
|
57
|
+
const LAND_TO_INK = {
|
|
58
|
+
landcover: 0.05,
|
|
59
|
+
built: 0.07,
|
|
60
|
+
paved: 0.09,
|
|
61
|
+
buildings: 0.12,
|
|
62
|
+
path: 0.18,
|
|
63
|
+
"road-minor": 0.26,
|
|
64
|
+
"road-medium": 0.38,
|
|
65
|
+
"road-major": 0.5,
|
|
66
|
+
boundary: 0.42,
|
|
67
|
+
rail: 0.56,
|
|
68
|
+
"label-neighbourhood": 0.72,
|
|
69
|
+
// 目印の名前は、町名と同じ控えめな段に置く。**これ以上濃いと駅と地名を食う**
|
|
70
|
+
"label-poi": 0.72,
|
|
71
|
+
"label-station": 0.86,
|
|
72
|
+
};
|
|
73
|
+
/** 緑を渡されなかったときの行き先。**紙の地図の公園の色に寄せた中間値** */
|
|
74
|
+
const GREEN_HINT = "#6f9e58";
|
|
75
|
+
/**
|
|
76
|
+
* 指し値から 26 の役割を作る。
|
|
77
|
+
*
|
|
78
|
+
* @param anchors 人が決める 4〜5 色
|
|
79
|
+
* @param overrides 手で決めた色。**ここが最後に勝つ**
|
|
80
|
+
*/
|
|
81
|
+
export function deriveRoles(anchors, overrides = {}) {
|
|
82
|
+
// 読めない色はここで落とす。**黙って黒へ落とすと、間違いが黒い地図として出る**
|
|
83
|
+
const land = toHex(parseHex(anchors.land));
|
|
84
|
+
const water = toHex(parseHex(anchors.water));
|
|
85
|
+
const ink = toHex(parseHex(anchors.ink));
|
|
86
|
+
const accent = toHex(parseHex(anchors.accent));
|
|
87
|
+
const green = anchors.green === undefined ? mix(land, GREEN_HINT, 0.55) : toHex(parseHex(anchors.green));
|
|
88
|
+
const roles = {
|
|
89
|
+
background: land,
|
|
90
|
+
earth: land,
|
|
91
|
+
green,
|
|
92
|
+
water,
|
|
93
|
+
// 水路は水より一段濃い。同じ色だと、川筋が面に埋もれて見えなくなる
|
|
94
|
+
waterway: mix(water, ink, 0.28),
|
|
95
|
+
// 水の名前は水の系列から取る。陸のラベルと同じ色だと、何の名前か分からなくなる
|
|
96
|
+
"label-water": mix(water, ink, 0.62),
|
|
97
|
+
// **指し値そのもの**。作り変えない
|
|
98
|
+
highway: accent,
|
|
99
|
+
station: accent,
|
|
100
|
+
"station-fill": land,
|
|
101
|
+
"label-city": ink,
|
|
102
|
+
// 縁取りは地色。文字と同じ側にすると、ラベルが背景に溶ける
|
|
103
|
+
halo: land,
|
|
104
|
+
// 線路のハッチは**地色そのもの**。線路の上に地色の刻みを入れて「線路」に見せる。
|
|
105
|
+
// ここを地色から外すと、刻みが別の線に見えて道が 2 本あるように読める
|
|
106
|
+
"rail-hatch": land,
|
|
107
|
+
// 高速の縁取りは本体より濃い。**同じにすると太い線が塊になる**(Modern Ink で実際に潰れた)
|
|
108
|
+
"highway-casing": mix(accent, ink, 0.4),
|
|
109
|
+
};
|
|
110
|
+
for (const [role, t] of Object.entries(LAND_TO_INK)) {
|
|
111
|
+
roles[role] = mix(land, ink, t);
|
|
112
|
+
}
|
|
113
|
+
for (const [role, color] of Object.entries(overrides)) {
|
|
114
|
+
if (!ROLE_ORDER.includes(role)) {
|
|
115
|
+
// 綴り間違いを黙って捨てない。捨てると「指定したのに効かない」になる
|
|
116
|
+
throw new Error(`知らない役割です: ${role}(使えるのは ${ROLE_ORDER.join(" ")})`);
|
|
117
|
+
}
|
|
118
|
+
roles[role] = toHex(parseHex(color));
|
|
119
|
+
}
|
|
120
|
+
// 並びを固定して返す。**差分が読める形にするため**
|
|
121
|
+
const ordered = {};
|
|
122
|
+
for (const role of ROLE_ORDER)
|
|
123
|
+
ordered[role] = roles[role];
|
|
124
|
+
return ordered;
|
|
125
|
+
}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP の口(stdio)。
|
|
3
|
+
*
|
|
4
|
+
* pnpm palette:mcp
|
|
5
|
+
*
|
|
6
|
+
* Claude Code などの設定に、この起動コマンドを登録して使う。
|
|
7
|
+
* 詳しい登録の仕方は `tools/palette/README.md`。
|
|
8
|
+
*
|
|
9
|
+
* **stdout には応答しか書かない。**ログを 1 行でも混ぜると、
|
|
10
|
+
* 相手は JSON として読もうとして接続が壊れる(原因が非常に分かりにくい)。
|
|
11
|
+
* 出したいものは stderr へ。
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
14
|
+
import { createInterface } from "node:readline";
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { handle } from "./server.js";
|
|
18
|
+
/**
|
|
19
|
+
* 土台のスタイルを探す。**配った包みの中が先、リポジトリの根が後。**
|
|
20
|
+
*
|
|
21
|
+
* - 配ったとき: `<package>/styles/`(`prepublishOnly` が正本から写す)
|
|
22
|
+
* - リポジトリで動かすとき: `<repo>/styles/`(**正本はこちら**・D-002)
|
|
23
|
+
*
|
|
24
|
+
* **両方を見る。**どちらか一方にすると、
|
|
25
|
+
* 「手元では動くのに `npx` で動かない」か、その逆が起きる。
|
|
26
|
+
*/
|
|
27
|
+
function findStyleDir() {
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
// src/mcp.ts なら ../styles、dist/mcp.js でも ../styles(どちらも包みの直下)
|
|
30
|
+
const candidates = [resolve(here, "../styles"), resolve(here, "../../..", "styles")];
|
|
31
|
+
for (const dir of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
if (readdirSync(dir).some((name) => name.endsWith(".json")))
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 無い候補は次へ。**最後まで無ければ下で落とす**(黙って空で動かさない)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`土台のスタイルが見つかりません: ${candidates.join(" / ")}`);
|
|
41
|
+
}
|
|
42
|
+
const styleDir = findStyleDir();
|
|
43
|
+
/**
|
|
44
|
+
* 名乗る版を package.json から読む。**ここで数字を決め打たない。**
|
|
45
|
+
* 決め打つと、版を上げた日に名乗りだけ古いまま残る(気づけない)。
|
|
46
|
+
*/
|
|
47
|
+
function readVersion() {
|
|
48
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
for (const path of [resolve(here, "../package.json"), resolve(here, "../../package.json")]) {
|
|
50
|
+
try {
|
|
51
|
+
const value = JSON.parse(readFileSync(path, "utf8")).version;
|
|
52
|
+
if (typeof value === "string" && value !== "")
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// 次の候補へ。**両方だめなら下で分かる値を返す**
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return "0.0.0-unknown";
|
|
60
|
+
}
|
|
61
|
+
const deps = {
|
|
62
|
+
version: readVersion(),
|
|
63
|
+
listBases: () => readdirSync(styleDir)
|
|
64
|
+
.filter((name) => name.endsWith(".json"))
|
|
65
|
+
.map((name) => name.replace(/\.json$/, ""))
|
|
66
|
+
.sort(),
|
|
67
|
+
loadBase: (name) => {
|
|
68
|
+
// 名前を経路として扱わせない。**`../` を渡されて外を読ませない**(§21)
|
|
69
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name))
|
|
70
|
+
throw new Error(`土台の名前が不正です: ${name}`);
|
|
71
|
+
return JSON.parse(readFileSync(resolve(styleDir, `${name}.json`), "utf8"));
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const lines = createInterface({ input: process.stdin });
|
|
75
|
+
lines.on("line", (line) => {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (trimmed === "")
|
|
78
|
+
return;
|
|
79
|
+
let request;
|
|
80
|
+
try {
|
|
81
|
+
request = JSON.parse(trimmed);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// 読めない行に応答を返しようがない(id が分からない)。**握り潰さず stderr へ**
|
|
85
|
+
console.error("[mmj-palette] JSON として読めない行を無視しました");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const response = handle(request, deps);
|
|
89
|
+
if (response !== null)
|
|
90
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
91
|
+
});
|
|
92
|
+
lines.on("close", () => process.exit(0));
|
|
93
|
+
console.error(`[mmj-palette] MCP を stdio で待っています(土台: ${deps.listBases().join(" ")})`);
|
package/dist/oklab.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 色を混ぜる。**ここは純粋関数。**
|
|
3
|
+
*
|
|
4
|
+
* **sRGB のまま混ぜない。**理由は 2 つ。
|
|
5
|
+
*
|
|
6
|
+
* 1. **階調の間隔が目の感じ方と合わない。**道の 6 段のような等間隔の階調を sRGB で作ると、
|
|
7
|
+
* 明るい側が詰まって 2 段ぶんが同じに見える
|
|
8
|
+
* 2. **混ぜるとくすむ。**赤と緑を sRGB で平均すると `#808000` の濁った黄土色になる
|
|
9
|
+
*
|
|
10
|
+
* Oklab(Björn Ottosson・2020)は、目の感じ方に近い間隔で並ぶ色空間で、
|
|
11
|
+
* 明度の階調と色みの保持がどちらも素直に出る。**依存は足していない**(式だけ)。
|
|
12
|
+
* 出典: https://bottosson.github.io/posts/oklab/
|
|
13
|
+
*/
|
|
14
|
+
const HEX = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
15
|
+
/**
|
|
16
|
+
* `#rrggbb` / `#rgb` を読む。**読めなければ投げる。**
|
|
17
|
+
* 黒へ落とすと、綴り間違いが「黒い地図」として出てしまい、原因を探せない。
|
|
18
|
+
* @param value
|
|
19
|
+
*/
|
|
20
|
+
export function parseHex(value) {
|
|
21
|
+
if (typeof value !== "string" || !HEX.test(value.trim())) {
|
|
22
|
+
throw new Error(`色として読めません: ${JSON.stringify(value)}(#rrggbb か #rgb)`);
|
|
23
|
+
}
|
|
24
|
+
const body = value.trim().replace(/^#/, "");
|
|
25
|
+
const full = body.length === 3
|
|
26
|
+
? body
|
|
27
|
+
.split("")
|
|
28
|
+
.map((c) => c + c)
|
|
29
|
+
.join("")
|
|
30
|
+
: body;
|
|
31
|
+
const n = Number.parseInt(full, 16);
|
|
32
|
+
return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 };
|
|
33
|
+
}
|
|
34
|
+
/** 0〜1 の外は端で止める。**回り込ませない**(255 を越えて 0 に戻ると別の色になる) */
|
|
35
|
+
function clamp(value) {
|
|
36
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
37
|
+
}
|
|
38
|
+
/** `#rrggbb` に戻す。小文字で揃える(差分の見た目を安定させるため) */
|
|
39
|
+
export function toHex(rgb) {
|
|
40
|
+
const part = (value) => Math.round(clamp(value) * 255)
|
|
41
|
+
.toString(16)
|
|
42
|
+
.padStart(2, "0");
|
|
43
|
+
return `#${part(rgb.r)}${part(rgb.g)}${part(rgb.b)}`;
|
|
44
|
+
}
|
|
45
|
+
/** sRGB のガンマを外して線形にする */
|
|
46
|
+
function toLinear(c) {
|
|
47
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
48
|
+
}
|
|
49
|
+
/** 線形からガンマを戻す */
|
|
50
|
+
function fromLinear(c) {
|
|
51
|
+
return c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
|
|
52
|
+
}
|
|
53
|
+
function toLab(rgb) {
|
|
54
|
+
const r = toLinear(rgb.r);
|
|
55
|
+
const g = toLinear(rgb.g);
|
|
56
|
+
const b = toLinear(rgb.b);
|
|
57
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
58
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
59
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
60
|
+
return {
|
|
61
|
+
L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
62
|
+
a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
63
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function fromLab(lab) {
|
|
67
|
+
const l = (lab.L + 0.3963377774 * lab.a + 0.2158037573 * lab.b) ** 3;
|
|
68
|
+
const m = (lab.L - 0.1055613458 * lab.a - 0.0638541728 * lab.b) ** 3;
|
|
69
|
+
const s = (lab.L - 0.0894841775 * lab.a - 1.291485548 * lab.b) ** 3;
|
|
70
|
+
return {
|
|
71
|
+
r: fromLinear(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
72
|
+
g: fromLinear(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
73
|
+
b: fromLinear(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 2 色を混ぜる。`t` が 0 なら `from`、1 なら `to`。
|
|
78
|
+
* @param from 色(`#rrggbb`)
|
|
79
|
+
* @param to 色(`#rrggbb`)
|
|
80
|
+
* @param t 0〜1
|
|
81
|
+
*/
|
|
82
|
+
export function mix(from, to, t) {
|
|
83
|
+
// 端はそのまま返す。**丸め誤差で `#ffffff` が `#fffffe` にならないように**
|
|
84
|
+
if (t <= 0)
|
|
85
|
+
return toHex(parseHex(from));
|
|
86
|
+
if (t >= 1)
|
|
87
|
+
return toHex(parseHex(to));
|
|
88
|
+
const a = toLab(parseHex(from));
|
|
89
|
+
const b = toLab(parseHex(to));
|
|
90
|
+
return toHex(fromLab({
|
|
91
|
+
L: a.L + (b.L - a.L) * t,
|
|
92
|
+
a: a.a + (b.a - a.a) * t,
|
|
93
|
+
b: a.b + (b.b - a.b) * t,
|
|
94
|
+
}));
|
|
95
|
+
}
|