紡Book の勉強ノートに戻る
開発記録2026.06.15

AI が作った『星座』レイアウト、線が写真につながっていなかった — 座標系が2つあった話

フロントエンドCSSSVGAI活用紡Book

画像から起こしてもらったテンプレを直す中で気づいた「見た目と構造の境界」を、判断の置きどころ中心にまとめた記録です。

綴Photo の web 版に「星座」というページレイアウトを足している。中央に1枚、まわりに数枚の写真を散らして、細い線でゆるくつなぐ——記憶の相関図みたいな見た目。採用したモック画像を渡して、AI に実装してもらった。

返ってきた見た目は、ぱっと見それっぽかった。写真が散って、線も引かれている。でも実際に自分のページで開いてみたら、線がどこにもつながっていなかった。中央のあたりから曲線が何本か出ているのに、その端は写真の手前で途切れたり、写真を通り越したり、宙に浮いている。星座の「点と点を結ぶ」が成立していない。

この記事は、なぜ線が合わなかったのか(座標系が2つあった)、端点を手で寄せる直し方では直らなかった理由、そして座標を1か所に集約して直した過程をまとめる。

この記事で扱う観察ポイント

  • 「線が数px ずれている」ように見えて、実は別々の座標系で配置されていました
  • 端点を手で寄せる対症療法は、画面幅が変わるとまた崩れます
  • ノードと線が同じ座標を参照すれば、ズレは原理的に消えます

観察①: 線とノードが、別々の座標系にいた

最初に渡ってきた実装を読んだら、線とノードがまったく別の仕組みで置かれていた。

線は、座標を直接書いた SVG だった。

tsx
// 線:1000×720 の座標系に、端点を直接書いている
<svg viewBox="0 0 1000 720">
  <path d="M 500 330 C 425 275 350 250 285 220" />
  {/* ...あと5本、すべて手書きの座標... */}
</svg>

一方、写真(ノード)は CSS grid で置かれていた。

css
.web { display: grid; grid-template-columns: 9rem 13rem 36rem 13rem 9rem; }
.nodeA { grid-column: 2; grid-row: 1; } /* 左上のセル */

線の端点 285 220 は「1000×720 の座標系での位置」で、ノードの grid-column: 2 は「グリッドの2列目」。この2つは互いに何の関係もありません。線は「だいたいこの辺にノードがあるだろう」という当て込みで座標が書かれていて、ノードの実際の位置(grid が計算する)とは無関係だった。だから線の端は写真に届きません。

学びポイント

  • 「線がちょっとずれている」ように見えても、原因が「微調整不足」とは限りません。そもそも別の座標系で置かれていることがあります
  • 見た目が「それっぽい」と、この種の構造のズレは気づきにくいです。実際に動かす(画面幅を変える・データを差し替える)と露呈します

観察②: 端点を手で寄せる直し方は、すぐまた崩れる

最初の直しは、AI に「線の端点が写真の外に出ているから内側に寄せて」と頼むものだった。確かに、その時表示していた画面では端点が写真に近づいた。

でもこれは効かない直し方だと、すぐに気づいた。端点の座標は相変わらず手書きの固定値で、ノードは相変わらず grid。画面幅を変えれば grid のセルが動いて、また端点がずれます。実際、直してくれた AI 自身が「レイアウトを変えるたびに端点の微調整が必要」と言っていた。それは裏を返すと「構造としては合っていない」という告白だった。

ここで方針を変えると決めた。数px の寄せを繰り返すのではなく、線とノードが同じ座標を見る構造に作り替える。

学びポイント

  • 「その画面では直った」は、レスポンシブでは「直っていない」と同義になりえます
  • 微調整がエンドレスに必要な状態は、たいてい構造が間違っているサイン。手を動かす前に「なぜ毎回ずれるのか」を疑います

観察③: 座標を1か所に集約して、ノードと線で共有する

作り替えの方針はシンプルで、座標の定義を1か所に置き、ノードの配置も線の端点も、その同じ座標から導く。AI にこの方針で実装し直してもらった。

tsx
// 座標はここだけ。node の配置も line の端点も、この同じ値から導く
const CENTER = { x: 50, y: 46 };
const SLOT_DEFS = [
  { x: 20, y: 20 }, { x: 80, y: 18 },
  { x: 14, y: 52 }, { x: 86, y: 50 },
  { x: 26, y: 82 }, { x: 78, y: 80 },
];

ノードは、この座標を CSS 変数(CSS の中で使い回せる名前付きの値。ここでは座標の数字に --nx という名前を付けて CSS 側に手渡しています)で受け取って、画面の好きな位置にピンで留めるように置きます(絶対配置)。

tsx
<figure style={{ "--nx": `${slot.x}%`, "--ny": `${slot.y}%` }}>
css
.node {
  position: absolute;
  left: var(--nx); /* SLOT_DEFS の x がそのまま左位置になる */
  top: var(--ny);
  transform: translate(-50%, -50%); /* 座標を写真の中心に合わせる */
}

線も、同じ CENTER と SLOT_DEFS から引きます。

tsx
<svg
  viewBox="0 0 100 100"          {/* 0〜100 の座標系にする → CSS の 0%〜100% とそのまま対応する */}
  preserveAspectRatio="none"    {/* 縦横比を無視して要素いっぱいに引き伸ばす → viewBox の 50 = 要素の幅50% */}
>
  {SLOT_DEFS.slice(0, count).map((s, i) => (
    // 中央 → 各ノードへ。端点はノードと同じ座標
    <path key={i} d={`M ${CENTER.x} ${CENTER.y} L ${s.x} ${s.y}`}
          vectorEffect="non-scaling-stroke" {/* 引き伸ばしで線の太さが歪むのを防ぐ */}
    />
  ))}
</svg>

肝は SVG の viewBox="0 0 100 100" と preserveAspectRatio="none" だった。

これで、CSS の left: var(--nx)(%)と SVG の x = s.x(0–100)が同じ座標系に乗った。座標を1個動かせば、写真も線の端点も一緒に動きます。端点のズレは「微調整で近づける」ものではなく、原理的に発生しないものになった。

写真の枚数が変わっても線が追従するのも、同じ仕組みのおかげです。線は SLOT_DEFS.slice(0, count) で「ノードの数だけ」引かれます。6枚を超えたぶんは星座にせず下に流す、という割り切りも決めた(点が多すぎると星座に見えないので)。

学びポイント

  • 位置を複数の要素で合わせたいときは、座標の出どころを1つにします。各要素はそこを参照するだけにします
  • SVG と CSS を重ねて位置を合わせるなら、viewBox + preserveAspectRatio="none" で座標系を CSS の % に揃えると、両者が同じ物差しになります

このテーマの括り

今回いじったのは見た目のコードだけど、効いたのは見た目の調整ではなく座標系の設計だった。線とノードを別々に置いていたのを、同じ座標を参照する形に変えた。それだけで「毎回ずれる」が「ずれようがない」に変わった。

そして、この一件で AI との分担がはっきりした。見た目の叩き台を一気に出すのは AI が速いです。写真を散らして線を引いて、それっぽい画面を作るところまではすぐ返ってきます。でも「線がノードと幾何的に一致する」「画面幅や枚数が変わっても破綻しない」みたいな構造は踏み外します。そこは、ズレに気づいて、原因(座標系が2つ)を見つけて、「座標を1本化する」と方針を決める——この判断の側を自分が持つ必要があった。実装そのものは AI に書いてもらっても、どう作るかを決めるのは別の仕事だった。


この記事は2026年6月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。