柴犬で集まれワン・ワンは愛犬画像をアップロードして、両隣の画像登録者とのチャットを楽しむシステムです。この公開システムは基本構造のみで、クライアント側とサーバー側を介して行う事を前提に構築の選択を選べるように基礎設定の段階となっています。

基本説明
リングに1~34の画像設定があり、好きな位置にペットの画像を貼り付けて、ご自身の画像両隣の飼い主とチャットを楽しむという設定です。画面には表示されていませんが、リングの下に両隣を含めた3つの画像と、3人分を一つのチャット画面が表示される仕組みを想定しています。画像登録サイズは400×600PXが推奨(フォーム表示は縮小200×268px)されます。また、とりあえず34人分の登録ですが、XとY座標の調整で数の増減は可能です。
主な仕様
1.運営管理者にメールを送信
2.メール内容の記載されたIDを取得
3.ID(英文字・英数字を含めた8文字)を入力
4.メールアドレスを登録
5.1~34までの好きな位置に画像をアップロードして登録
6.両隣を含めた3枚の画像を表示
7.両隣の3人の飼い主と一つのチャット画面でチャット
ご利用について本プログラムは初期段階の構築です。全てのクライアント様がチャットできる環境はそれぞれのシステム環境が考えられるので、現在のコードでは作成していません。なので、この仕様を参考にする場合は、改造・変更・削除・修正・追加・デザイン変更等々は制限ありません。また、コピー&ペースト等による実際の不具合等が発生致しても作成者は責任を問う事は致しません。
※変更なしでご使用された場合は、どこか小さく片隅にMOMOPLANと書いて頂くと嬉しいです。
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>柴犬の輪登録</title>
  <link rel="stylesheet" href="dog_registration.css">
</head>
<body>
  <div class="wanwan">柴犬でワン・ワン・ワン</div>
  <br>
  <div class="circle-container" id="circle">
  <img class="srogo" src="howto.png" alt=""/></div>
  <form id="upload-form"><div>
    <label for="image-upload">画像をアップロード</label>
    <input type="file" id="image-upload" accept=".jpg, .png">
    <button type="button" id="submit-button">登録</button>
    <textarea id="comment-input" placeholder="コメントを書く" ></textarea>
    <button type="button" id="submit-comment">送信</button>
  </form>
  <form id="registration-form">
    <div>
      <label for="id-input">IDを入力してください (英文字4文字+数字4文字)</label>
      <input type="text" id="id-input" maxlength="8" placeholder="例: test1234">
      <button type="button" id="register-button" disabled>登録</button>
    </div>
    <div id="email-container" style="display: none;">
      <label for="email-input">メールを入力してください (例:admin@admin.com):</label>
      <input type="email" id="email-input" placeholder="例: example@domain.com">
      <button type="button" id="submit-button2" disabled>送信</button>
    </div>
  </form>
  <div id="message-container"><FONT color="#0066ff">お好きな位置(数字)を選んでください</FONT></div>
  <button type="button"id="allset" onclick="formReset()">リセット</button>
  <!-- コメント表示領域 -->
  <div id="comment-container" style="margin-top: 20px; padding: 10px;"></div>
  <script src="do_registration.js"></script>
</body>
</html>
JS
// 初期化処理(間隔調整済み)
document.addEventListener("DOMContentLoaded", () => {
  const containerId = "circle";
  const pointCount = 34; // ポイント数
  const radius = 250;    // 半径
  const submitButton = document.getElementById("submit-comment");
  const commentInput = document.getElementById("comment-input");
  const commentContainer = document.getElementById("comment-container");
  const idInput = document.getElementById("id-input");
  const registerButton = document.getElementById("register-button");
  const emailContainer = document.getElementById("email-container");
  const emailInput = document.getElementById("email-input");
  const submitButton2 = document.getElementById("submit-button2");
  const fileInput = document.getElementById("image-upload");
  /*const uploadForm = document.getElementById("upload-form");*/

// 初期状態
fileInput.disabled = true; // アップロードフォームを無効化
registerButton.disabled = true; // 登録ボタン無効化
submitButton2.disabled = true; // 送信ボタン無効化

  // ID入力欄の変更監視
  idInput.addEventListener("input", () => {
    const idValue = idInput.value.trim();
    const idPattern = /^[a-zA-Z]{4}[0-9]{4}$/; //英文字4文字+数字4文字の正規入力入力
    if (idPattern.test(idValue)) {
      registerButton.disabled = false; // ボタン有効化
      emailContainer.style.display = "block"; // メール欄表示
    } else {
      registerButton.disabled = true; // ボタンの無効化
      emailContainer.style.display = "none"; // メール欄の非表示
    }
  });

// メール入力欄の変更監視
emailInput.addEventListener("input", () => {
  const emailValue = emailInput.value.trim();
  const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; // ドメインフォーマットの正規入力
  if (emailPattern.test(emailValue)) {
    submitButton2.disabled = false; // 送信ボタンの有効化
  } else {
    submitButton2.disabled = true; // 送信ボタンの無効化
  }
});

// 登録ボタンのクリック処理
  registerButton.addEventListener("click", () => {
    alert("登録ボタンがクリックされました。");
  });

  // 送信ボタンのクリック処理
  submitButton2.addEventListener("click", () => {
    const emailValue = emailInput.value.trim();
    if (emailValue) {
      alert(`メール登録完了: ${emailValue}`);

      // アップロードフォームを有効化
      fileInput.disabled = false;
      console.log("アップロードフォームが有効になりました"); // デバッグ用ログ

      // 入力欄のリセット
      emailContainer.style.display = "none"; // メール入力欄を非表示にする
      idInput.value = ""; // ID入力欄クリア
      emailInput.value = ""; // メール入力欄クリア
      submitButton2.disabled = true; // 送信ボタンの再無効化
      registerButton.disabled = true; // 登録ボタンの再無効化

      // setupImageUpload呼び出しをここで有効化
      setupImageUpload("upload-form", "image-upload", "submit-button", containerId, pointCount, radius);
    } else {
      alert("メールを入力してください!");
    }
  });

  // コメント送信処理
  submitButton.addEventListener("click", () => {
    const commentText = commentInput.value.trim(); // コメント内容を取得

    if (commentText) {
      // 枠付きのコメント要素を作成
      const comment = document.createElement("div");
      comment.innerText = commentText; // コメントの内容を設定
      comment.style.border = "thick double #32a1ce"; // 枠を設定
      comment.style.padding = "10px"; // 枠内の余白を設定
      comment.style.marginBottom = "10px"; // コメント間の余白を設定
      comment.style.borderRadius = "10px"; // 枠を角丸にする
      comment.style.width = "50%"// 枠の幅調整
      comment.style.margin = "0 auto"// 枠を中央
      commentContainer.appendChild(comment); // コメントを表示領域に追加

      // 入力欄をクリア
      commentInput.value = "";
    } else {
      alert("コメントを入力してください。");
    }
  });
  // すべてのポイントを空として生成
  for (let i = 0; i < pointCount; i++) {
    createEmptyPoint(containerId, i, pointCount, radius);
  }

  // アップロードフォームのイベントを設定
  /*setupImageUpload("upload-form", "image-upload", "submit-button", containerId, pointCount, radius);*/
});

function resizeAndCropImage(inputPath, outputPath) {
  sharp(inputPath)
    .resize(50, 50) // サイズ変更
    .composite([{ input: Buffer.from('<svg><circle cx="30" cy="30" r="30" fill="white"/></svg>'), blend: "dest-in" }]) // 円形にする
    .toFile(outputPath)
    .then(() => {
      console.log("画像リサイズと円形加工が完了しました!");
    })
    .catch(err => {
      console.error("エラー:", err);
    });
}

// 円状にリサイズされた画像をポイントに表示する
const pointImages = {}; // 各ポイントの画像情報を保持

function displayImageAtPoint(containerId, imageUrl, pointIndex, pointCount, radius) {
  const container = document.getElementById(containerId);
  const angle = (pointIndex * (360 / pointCount)) * (Math.PI / 180);
  const x = radius + radius * Math.cos(angle) - 30;
  const y = radius + radius * Math.sin(angle) - 30;

  const point = document.createElement("div");
  point.style.position = "absolute";
  point.style.left = `${x}px`;
  point.style.top = `${y}px`;

  const img = document.createElement("img");
  img.src = imageUrl;
  img.alt = `ポイント${pointIndex + 1}`;
  img.style.width = "50px";
  img.style.height = "50px";
  img.style.borderRadius = "60%";

  point.appendChild(img);
  container.appendChild(point);

  // **既存のポイント情報がある場合は上書きせず保持**
  if (!pointImages[pointIndex]) {
    pointImages[pointIndex] = imageUrl;
  }
}

// 空の円状ポイントを作成する関数
function createEmptyPoint(containerId, pointIndex, pointCount, radius) {
  const container = document.getElementById(containerId);
  const angle = (pointIndex * (360 / pointCount)) * (Math.PI / 180); // 角度計算
  const x = radius + radius * Math.cos(angle) - 25; // X座標
  const y = radius + radius * Math.sin(angle) - 25; // Y座標
  // ポイント要素を作成
  const point = document.createElement("div");
  point.className = "point";
  point.style.position = "absolute";
  point.style.left = `${x}px`;
  point.style.top = `${y}px`;
  point.style.width = "40px";
  point.style.height = "40px";
  // 数字を表示
  point.innerText = pointIndex + 1;
  // コンテナにポイントを追加
  container.appendChild(point);
  // クリックイベントで画像を追加
  point.addEventListener("click", () => {
  const fileInput = document.getElementById("image-upload");
  const button = document.getElementById("submit-button");
  // 選択したポイントを記録
  button.dataset.targetPoint = pointIndex;
  // "alert" を innerText に変更
  const messageContainer = document.getElementById("message-container");
  messageContainer.innerText = `ポイント${pointIndex + 1}が選択されました。IDを入力してから画像をアップロードしてください。`;
  });
  container.appendChild(point);
}

// 画像アップロード処理
function setupImageUpload(formId, inputId, buttonId, containerId, pointCount, radius) {
  const form = document.getElementById(formId);
  const fileInput = document.getElementById(inputId);
  const button = document.getElementById(buttonId);

  button.addEventListener("click", async () => {
    const file = fileInput.files[0];
  
    if (file) {
      const imageUrl = URL.createObjectURL(file);
      const targetPoint = parseInt(button.dataset.targetPoint, 10);
      
      displayImageAtPoint(containerId, imageUrl, targetPoint, pointCount, radius);
      displayNeighborImages(containerId, imageUrl, targetPoint, pointCount, radius);
    } else {
      alert("ファイルをアップロードしてください。");
    }
  });  
}
function formReset() {
  location.reload();
}

// コメント表示やポイント生成の処理の後に以下を追加
function displayNeighborImages(containerId, imageUrl, pointIndex, pointCount, radius) {
  const neighborContainer = document.getElementById("neighbor-images-container");
  neighborContainer.innerHTML = "";

  // **前後2つのポイントを取得**
  const neighbors = [
    (pointIndex - 2 + pointCount) % pointCount, // 2つ前
    (pointIndex - 1 + pointCount) % pointCount, // 1つ前
    pointIndex, // 現在のポイント
    (pointIndex + 1) % pointCount, // 1つ後
    (pointIndex + 2) % pointCount // 2つ後
  ];

  neighbors.forEach(neighborIndex => {
    let neighborImage = getImageForPoint(neighborIndex);
    const img = document.createElement("img");

    if (!neighborImage) {
      return; // **画像がない場合は表示しない**
    }

    img.src = neighborImage;
    img.alt = `ポイント${neighborIndex + 1}`;
    img.style.width = "200px";
    img.style.height = "268px";
    img.style.margin = "0 10px";
    img.style.border = "1px solid black";
    neighborContainer.appendChild(img);
  });
}

function getImageForPoint(pointIndex) {
  const point = document.querySelector(`[alt='ポイント${pointIndex + 1}']`);
  if (point && point.src) {
    return point.src;
  } else {
    return null; // 画像がない場合
  }
}
CSS
body {
  margin: auto;
  background-image: url("s-brick008.gif");
   }

/* 円形コンテナのスタイル */
.circle-container {
  position: relative;
  width: 500px;
  height: 500px;
  border: 1px solid #ccc;
  border-radius: 50%;
  margin: 20px auto;
}

.point img {
  width: 60px;
  height: 60px;
  border-radius: 50%; /* 円形にする */
}

/* ポイントスタイル */
.point {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: rgb(223, 240, 245);
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  transition: background-color 0.3s;
}

.point:hover {
  background-color: #ffcc00;
}

/* フォーム全体のスタイル */
#upload-form {
  text-align: center;
  margin-top: 20px;
}

#image-upload {
  margin-right: 10px;
}

button {
  padding: 8px 12px;
  background-color: #007BFF;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #0056b3;
}

#message-container{
  text-align: center;
  margin-top: 20px;
  font-weight: bold;
  color: red;
}

#comment-input{
  position: relative;
  width: 150px;
  height: 200px;
  height: 30px;
  top: 12px;
  left: 2px;
  border-radius: 4px;
  resize: horizontal;
 }

 #submit-comment{
  position: relative;
  width: 60px;
  height: 34px;
  background-color: #079452;
  left: 4px;
  border-radius: 4px;
  resize: horizontal;
 }

 .srogo{
  position: absolute;
  top: 144px;
  left: 152px;
  resize: horizontal;
}

label {
  font-weight: bold;
  font-size: 16px; color: rgb(60, 20, 206);
  text-shadow: 2px 1px 2px #7f8da1;
}
#allset{
  background-color: #ff6b6b;
  color: rgb(0, 0, 0);
  font-weight: bold;
  border-radius: 4px;
 }

 .wanwan {
  text-align:center;
  font-weight: bold;
  font-size: 26px; color: rgb(255, 100, 203);
  text-shadow: 2px 1px 2px #19191a;
}

以上です。。。デモページのIDは前4英文字、後4数字です