Newer
Older
2025-shino / fog.js
(() => {
  'use strict';

  document.addEventListener("DOMContentLoaded", () => {

    // =============================
    // 設定
    // =============================
    const START_POS = [38.891, 139.824];
    const REVEAL_RADIUS = 40;       // m
    const MAX_EXPLORE_POINTS = 300; // 制圧率100%

    // =============================
    // DOM
    // =============================
    const titleEl = document.getElementById("title");
    const startBtn = document.getElementById("start");
    const stopBtn  = document.getElementById("stop");
    const controls = document.getElementById("controls");

    // 制圧率表示(HTMLに無いので作る)
    const rateEl = document.createElement("span");
    rateEl.textContent = " 制圧率: 0%";
    rateEl.style.marginLeft = "12px";
    rateEl.style.fontWeight = "bold";
    controls.appendChild(rateEl);

    // =============================
    // 状態
    // =============================
    let map;
    let playerMarker;
    let watchId = null;

    // 解除済みポイント
    const revealedPoints = [];

    // =============================
    // Fog Layer
    // =============================
    const FogLayer = L.GridLayer.extend({
      createTile: function (coords) {
        const tile = document.createElement("canvas");
        const size = this.getTileSize();
        tile.width = size.x;
        tile.height = size.y;

        const ctx = tile.getContext("2d");

        // 霧
        ctx.fillStyle = "rgba(0,0,0,0.85)";
        ctx.fillRect(0, 0, size.x, size.y);

        const bounds = this._tileCoordsToBounds(coords);
        ctx.globalCompositeOperation = "destination-out";

        revealedPoints.forEach(latlng => {
          if (!bounds.contains(latlng)) return;

          const point = map.latLngToContainerPoint(latlng);
          const tilePoint = map.latLngToContainerPoint(bounds.getNorthWest());

          const x = point.x - tilePoint.x;
          const y = point.y - tilePoint.y;

          ctx.beginPath();
          ctx.arc(
            x,
            y,
            metersToPixels(REVEAL_RADIUS, latlng.lat),
            0,
            Math.PI * 2
          );
          ctx.fill();
        });

        return tile;
      }
    });

    let fogLayer;

    // =============================
    // Utility
    // =============================
    function metersToPixels(m, lat) {
      return (
        m / 40075017 *
        256 *
        Math.pow(2, map.getZoom()) /
        Math.cos(lat * Math.PI / 180)
      );
    }

    function isNearExisting(latlng, threshold = 10) {
      return revealedPoints.some(p => p.distanceTo(latlng) < threshold);
    }

    // =============================
    // 制圧率
    // =============================
    function updateExploreRate() {
      const rate = Math.min(
        100,
        Math.floor((revealedPoints.length / MAX_EXPLORE_POINTS) * 100)
      );
      rateEl.textContent = ` 制圧率: ${rate}%`;

      if (rate === 100) {
        titleEl.textContent = "完全制圧!";
      }
    }

    // =============================
    // Map初期化
    // =============================
    function initMap() {
      map = L.map("locationmap").setView(START_POS, 16);

      L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
        attribution: "&copy; OpenStreetMap contributors"
      }).addTo(map);

      fogLayer = new FogLayer();
      fogLayer.addTo(map);

      playerMarker = L.marker(map.getCenter()).addTo(map);
      playerMarker.bindPopup("STARTで探索開始").openPopup();

      // デバッグ用クリック探索
      map.on("click", e => updateLocation(e.latlng));
    }

    // =============================
    // 霧解除
    // =============================
    function reveal(latlng) {
      // 同じ場所での重複解除防止
      if (isNearExisting(latlng)) return;

      revealedPoints.push(latlng);
      fogLayer.redraw();
      updateExploreRate();
    }

    // =============================
    // 位置更新
    // =============================
    function updateLocation(latlng) {
      map.panTo(latlng, { animate: false });
      playerMarker.setLatLng(latlng);
      reveal(latlng);
    }

    // =============================
    // GPS
    // =============================
    function onSuccess(pos) {
      updateLocation(
        L.latLng(pos.coords.latitude, pos.coords.longitude)
      );
    }

    function startWatch() {
      stopWatch();
      watchId = navigator.geolocation.watchPosition(
        onSuccess,
        null,
        { enableHighAccuracy: true }
      );
      titleEl.textContent = "探索中";
    }

    function stopWatch() {
      if (watchId !== null) {
        navigator.geolocation.clearWatch(watchId);
        watchId = null;
      }
      titleEl.textContent = "霧解除探索ゲーム";
    }

    // =============================
    // Event
    // =============================
    startBtn.addEventListener("click", startWatch);
    stopBtn.addEventListener("click", stopWatch);

    // =============================
    // Start
    // =============================
    initMap();
  });
})();