→ ブラウザで開く
異世界小説用の地図を作るためのブラウザツール。地図画像を読み込むと色から地形を自動生成し、その上を六角タイルで手直しして、施設・街道・テキストを重ねていく。
中身は dist/index.html 1枚だけです。 ビルド不要・依存なし・外部通信なし。ダウンロードしてブラウザで開けば動きます。データは端末内(localStorage)に自動保存し、JSONで書き出せます。
上のリンクを開くか、dist/index.html をダウンロードしてブラウザで開く。初回は小さな島が置かれた状態から始まる。
- 画像から生成 — 画像を読み込む(キャンバスへのドラッグ&ドロップでも可)。読み込んだ瞬間に色を判定して地形が決まる。「縦・横」で1枚あたりのマス数、「高さも自動でつける」の有無を決めて「設定を変えてやり直す」で再実行できる。ノイズの平滑化は常時かかる
- 「大きさ」で1枚あたりのマス数を決める。既存の地形には重ねない。海のままの空き地を左上から探し、無ければキャンバスを右へ広げて置く
- なので大陸の西と東を別々の画像で読み込める。並んだあとは「範囲を動かす」で位置を組み替える
- キャンバス — 縦(行数)・横(列数)を入れて「適用」でグリッドを広げる/縮める(各4〜200)。中心を保ったまま伸縮し、増えた分は海になる。縮めて範囲外に出る施設・テキスト・経路があると件数を出して確認する
- 地形ブラシ — 14種の地形をクリック/ドラッグで塗る。筆の大きさ1〜3(それぞれ1・7・19マス)
- 高さ調整 — 盛る/削るで段丘を0〜4段の範囲で変える
- 施設 — 城・都市・町・砦・港・遺跡・神殿・ダンジョン・塔の9種を配置
- 経路 — 街道/川/航路。マスを順にクリックしてEnterか「確定」。街道と川は同じ接続モデルで描かれ、交差すると街道が上を通る(橋のように見える)
- テキスト — クリックで配置、右パネルで文字を入力。大きさ5段階(12.5〜36px)、色6種、下敷きの有無を選べる。地名に限らず国名・山脈名などに使う
- 下敷きと縁取りの色は文字色の明るさから自動で反転する。白い文字を雪原に置いても読める
- 施設は名前を持たない。置いた直後に選択もしないので、密集させて連続で置ける
- 右ドラッグ/中ドラッグで移動、ホイールで拡大縮小、
Ctrl+Z/Cmd+Zで元に戻す(40段) - スマホ・タブレットは 1本指で描く/2本指で移動とピンチで拡大縮小。道具箱は左上の ☰ で下から開き、道具を選ぶと自動で閉じる(筆の大きさなどを選んでも閉じない)
Escで選択・経路作成をキャンセル- 「選択・移動」ツールでテキスト・施設をドラッグして位置を微調整
- 「範囲を動かす」ツールでドラッグして矩形を囲み、その中をドラッグすると地形ごと移動する。元の場所は海に戻る。施設・テキストは一緒に運ばれ、街道は動かない。上下は2マス単位
- 「消しゴム」はそのマスにある一番上のものを消す。施設・テキスト → 経路 → 地形の順。経路のあるマスでは地形を残すので、道だけ引き直せる。途中を消すと経路は前後に分割される
- 自動保存は
localStorage['worldmap-maker-v1']。キャッシュ削除・シークレットモード・容量超過で消える - 容量(数MB)を超えると読み込んだ画像だけ除いて保存し、「保存(画像は除く)」と表示する
- 確実な保管は「JSONで保存」。15分以上未保存の変更があると上部に赤字で警告する
- PNGは2倍解像度で書き出す
「PNGで書き出す」で出した画像を画像生成モデルに渡し、絵地図に起こす。
プロンプト例(地域固有の指定は自分の地図に合わせて書き換える):
このタイル地図をワールドマップ風のイラストに変換してください。
- 地形と施設の配置は完全に維持すること
- 茶色のタイルは丘陵
- 都市はそれぞれ建物として描き、大きな街は首都とする
- 海岸沿いの街は港町、砦は要塞として描く
- 左下は砂漠
- 北側は山岳だけでなく氷山も混ぜる
- 湖は大きくしすぎない
- 道路はある程度強調する
単一ファイル。<style> にトークンと部品、<script> に全ロジック。主要ブロックはコメントで区切っている。
- 地形とパラメータ —
TER(地形14種:色・既定の高さ・陸判定)、STR(施設9種)、幾何定数TERへの追加は必ず末尾に。 保存JSONのtiles[].kはこの配列の添字なので、途中に挿すと既存データの地形が全部ずれるSTRは逆に並べ替えてよい。 保存されるのはstructures[].typeの文字列キーなので、順序を変えても途中に挿しても既存データは壊れない(kの値を変えるのは駄目)Sタイル半径 /SQ縦方向の圧縮率(0.84=浅い俯瞰)/UNIT1段の高さ /TOPROOM上方向の余白
- 盤面の初期化 —
blankTiles(cols, rows)が全面の海(「全部消す」もこれ)。starterIsland()は初期表示用のざっくりした島で、中心からの距離を角度で揺らして地形を切っているだけ。ノイズを使わないので毎回同じ形 - 幾何 —
pos(c,r)タイル中心座標(pointy-top、奇数行を半マスずらす)、liftOf()段差の持ち上げ量、pick()画面座標→タイル(持ち上げ後の中心で最近傍探索)cellsAround(c,r,n)は半径 n リング内のタイルを返す。縦の圧縮を戻すと隣接距離は6方向ともHWちょうどで、リング n はn*HWに並ぶ。判定の余裕は半径に比例させること(固定値だと n が増えるほど取りこぼす)
- キャンバスサイズ —
resizeGrid(cols, rows)。中心を保って伸縮する。行のずらし量は偶数に丸める(pos()が奇数行だけ半マスずらすので、奇数ぶん動かすと全行の偶奇が反転して隣接関係が崩れる) - 施設アイコン —
drawStructure()。すべて canvas のパスで描く。画像素材なし- 光源は左上固定。
slab()(陸屋根)とbldg()+gable()(切妻)が、屋根の正面>正面壁>右側面の順に暗くして立体を作る - 都市・町は
cluster()が複数棟をばらまく。棟数・幅・高さ・色はhashRnd(c,r)由来なので、保存しなくても同じ街並みが再現される - タイルからはみ出してよい。広がりは
CLUSTER_SPREAD(1.0 でヘクスの頂点)、奥行きはDEPTHで調整する - 城は城壁→両翼の塔→天守の順(奥から手前)に描いて砦と差をつける。旗の先端は
-5.6u=約29pxで、上の余白TOPROOM(43px)に収まる範囲に留めること - ダンジョンは岩色が山地タイルと近いので、識別は黒い入口が担っている。岩だけ明るくしても山の上で消える
- 光源は左上固定。
- 地形の自動生成 —
classifyPixel(r,g,b)がRGB→HSLで地形1種を返し、autoTrace()が各ヘクスの中心+周囲6点の多数決で決める。ノイズ平滑化は常時、高さ付けは任意- 明度の高い画素は平原になる。紙地図の白い背景を陸として拾うため
- 書き換えるのは指定サイズのブロックだけ。
findFreeSpot()が海のままの空き地を探し、無ければensureSpot()がキャンバスを右へ継ぎ足す。継ぎ足しは左上基準(手動リサイズの中心基準とは別)なので既存タイルの座標が動かず、施設・テキスト・街道がずれない - 平滑化はブロック内で完結させ、高さ付けはブロック外の隣も見る。前者は隣ブロックの色に染まるのを防ぐため、後者は継ぎ目で段差が切れないため
- オフスクリーンはブロックの外接矩形ぶんだけ確保する。全面だと大きい盤で重い
- 範囲の移動 —
moveRegion()。矩形内のタイルを控えてから元を海にし、移動先へ書き戻す(移動元と移動先が重なっても壊れない)。行方向のずらし量はregionTarget()が偶数に丸める(cellsAroundと同じ理由で、奇数だと行の偶奇が入れ替わって地形が崩れる) - 描画 —
scene(g, live)が行ごとに「タイル → 街道 → 施設」を描き、最後にテキストをまとめて描く。live=falseでPNG書き出し用(選択枠・ホバーを描かない)- 行単位で交互に描くのは、施設がタイルからはみ出しても手前の行が上書きして奥行きを保つため。施設を全タイルのあとに一括で描くとこれが壊れる
- 街道 — 折れ線ではなく、マスごとの接続として描く。
linkに「どのマスがどの隣とつながっているか」を集め、各マスが中心から接続先との中点(=辺の中央)へ枝を伸ばす- 通過するだけのマスは中心を制御点にした二次ベジェ。両端が中心について対称なので直進は中心をぴたりと通り、曲がる所だけ角が丸まる。行き止まりと3叉以上は中心から直線の枝
- 別々に引いた街道でも同じマスを通れば自動的に合流して交差点になる
- 縁取りは不透明にすること。隣のマスの枝と辺の中央でちょうど突き合わさるので、半透明だと全マス境界で継ぎ目だけ濃くなる
- 各マスの街道は自分と接続先のうち最も手前の行で描く。地形は種類ごとに既定の段差を持つ(砂浜0・草地1・丘陵2・山地3・雪山4)ので、盛る/削るを使わなくても隣と高低差ができ、手前のマスが高いとその六角形が奥から伸びた枝を上塗りして街道が途切れる
- 残る限界: 2行手前に雪山(28px)があると、その六角形が2行ぶん(37.8px)を超えて奥へ届くため、隣接マスしか見ない今の方式では捕まえられない。完全に潰すには行ではなく描画深度で全要素をソートする必要がある
- 航路は接続モデルに乗せず、従来どおり折れ線(白い破線)のまま
- 読み込んだ画像は地形判定に使うだけで、キャンバスには重ねない。
drawUnderlay()とunderlayOpacity/underlayFrontはJSON互換のため残置しているが未使用
- 保存 —
serialize()/persist()(500msデバウンス)/snapshot()+undo()snapshot()はcols/rowsも積む。寸法だけ新しいまま古いtilesを戻すと参照が範囲外になるsetStatus()の文言はpersist()の「自動保存済み」に上書きされる。消えては困る告知はconfirm()を使うこと
- 編集操作 —
paintAt()、hitObject()、canvas の入力イベント群- 入力は Pointer Events に一本化。マウス・タッチ・ペンが同じ
startDrag()/moveDrag()/endDrag()を通るので、タッチ用の分岐を別に持たない - タッチは 1本指=描く/2本指=移動+ピンチ。2本目が着いた時点で
abortDrag()して1本指の操作を打ち切る(範囲移動は確定させずに捨てる) - 同時に触れている指は
touches配列。pointercancelを取りこぼすと指が残り、次のタップが「2本目」と誤判定されて描けなくなる。pointerdownで同じ id を差し替えて防ぐ - ピンチは2本指の中点でパンし、間隔の比を
zoomAt(f, px, py)に渡すだけ。zoomAtが元から焦点指定に対応しているので拡大側は書き足していない canvas { touch-action: none }は必須。 無いとブラウザがスクロールとピンチを先に取るのでイベントが届かない- 操作説明の文言は起動時のメディアクエリを初期値にし、以後は実際に来た
pointerTypeで上書きする(noteInput())。タッチとマウスが両方ある端末があるので一度きりの判定に頼らない
- 入力は Pointer Events に一本化。マウス・タッチ・ペンが同じ
- UI —
syncUI()が状態からボタンの active・ヒント文・インスペクタ表示を更新する。DOMは静的HTML+一部を生成- 幅700px以下ではサイドバーを下から出すドロワーにする(
aside.open)。position: fixedで流れから外すので、開閉してもキャンバスの寸法が変わらず、再描画もfit()のやり直しも要らない #appの高さは100dvh。iOS Safari の100vhは下のツールバーに隠れる分まで含むので、指定しないと画面下が切れる:hoverは@media (hover: hover)の中に置く。指だとタップ後に貼り付いて残る
- 幅700px以下ではサイドバーを下から出すドロワーにする(
{
v: 1,
cols, rows, // グリッド寸法
tiles: [[{k, tier}, ...], ...], // k = TER の添字, tier = 0..4
structures: [{c, r, type, dx, dy}], // 旧データの name は読み飛ばす
routes: [{type: 'road'|'sea', tiles: [[c,r], ...]}],
labels: [{c, r, text, size, color, plate, dx, dy}],
underlayOpacity, underlayFront, // 読み書きするが描画には未使用
image: "data:image/..." // 生成元画像(無ければ null)
}
dx/dy は表示位置の微調整オフセット(px、マップ座標系)。
テキストの size は LABEL_SIZES の添字。旧データは big: true/false しか持たないので、labelSize() が 1/0 に読み替える。color と plate は未指定なら既定(黒・下敷きなし)。
Modernist 系(赤 #ec3013 + 地色 #f3f2f2、角丸なし、2px罫線、フラッシュレフト)。タイルの配色は別系統で、ボードゲームの盤面に寄せたフラットな面色。
MIT。作った地図そのものは作者のもので、このライセンスは及ばない。