1. 程式人生 > 其它 >世界地圖(the world map)

世界地圖(the world map)

技術標籤:# 幾何圖形世界地圖world mapmap

世界地圖(the world map)


更多有趣示例 盡在 知屋安磚社群

示例

在這裡插入圖片描述

HTML

<div id="container">
      <canvas id="c"></canvas>
      <div id="labels"></div>
    </div>

CSS

body {
  margin: 0;
  font-family: sans-serif;
} #c { width: 100%; /* let our container decide our size */ height: 100%; display: block; } #container { position: relative; /* makes this the origin of its children */ width: 100vw; height: 100vh; overflow: hidden; } #labels { position: absolute; /* let us position ourself inside the container */
z-index: 0; /* make a new stacking context so children don't sort with rest of page */ left: 0; /* make our position the top left of the container */ top: 0; color: white; } #labels>div { position: absolute; /* let us position them inside the container */ left: 0; /* make their default position the top left of the container */
top: 0; cursor: pointer; /* change the cursor to a hand when over us */ font-size: small; user-select: none; /* don't let the text get selected */ pointer-events: none; /* make us invisible to the pointer */ text-shadow: /* create a black outline */ -1px -1px 0 #000, 0 -1px 0 #000, 1px -1px 0 #000, 1px 0 0 #000, 1px 1px 0 #000, 0 1px 0 #000, -1px 1px 0 #000, -1px 0 0 #000; } #labels>div:hover { color: red; }

JS

import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r108/build/three.module.js';
import {OrbitControls} from 'https://threejsfundamentals.org/threejs/resources/threejs/r108/examples/jsm/controls/OrbitControls.js';

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});

  const fov = 60;
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 10;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 2.5;

  const controls = new OrbitControls(camera, canvas);
  controls.enableDamping = true;
  controls.enablePan = false;
  controls.minDistance = 1.2;
  controls.maxDistance = 4;
  controls.update();

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('#246');

  const pickingScene = new THREE.Scene();
  pickingScene.background = new THREE.Color(0);

  const maxNumCountries = 512;
  const paletteTextureWidth = maxNumCountries;
  const paletteTextureHeight = 1;
  const palette = new Uint8Array(paletteTextureWidth * 3);
  const paletteTexture = new THREE.DataTexture(
      palette, paletteTextureWidth, paletteTextureHeight, THREE.RGBFormat);
  paletteTexture.minFilter = THREE.NearestFilter;
  paletteTexture.magFilter = THREE.NearestFilter;
  for (let i = 1; i < palette.length; ++i) {
    palette[i] = Math.random() * 256;
  }
  // set the ocean color (index #0)
  palette.set([100, 200, 255], 0);
  paletteTexture.needsUpdate = true;

  {
    const loader = new THREE.TextureLoader();
    const geometry = new THREE.SphereBufferGeometry(1, 64, 32);

    const indexTexture = loader.load('https://threejsfundamentals.org/threejs/resources/data/world/country-index-texture.png', render);
    indexTexture.minFilter = THREE.NearestFilter;
    indexTexture.magFilter = THREE.NearestFilter;

    const pickingMaterial = new THREE.MeshBasicMaterial({map: indexTexture});
    pickingScene.add(new THREE.Mesh(geometry, pickingMaterial));

    const fragmentShaderReplacements = [
      {
        from: '#include <common>',
        to: `
          #include <common>
          uniform sampler2D indexTexture;
          uniform sampler2D paletteTexture;
          uniform float paletteTextureWidth;
        `,
      },
      {
        from: '#include <color_fragment>',
        to: `
          #include <color_fragment>
          {
            vec4 indexColor = texture2D(indexTexture, vUv);
            float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
            vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
            vec4 paletteColor = texture2D(paletteTexture, paletteUV);
            // diffuseColor.rgb += paletteColor.rgb;   // white outlines
            diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
          }
        `,
      },
    ];

    const texture = loader.load('https://threejsfundamentals.org/threejs/resources/data/world/country-outlines-4k.png', render);
    const material = new THREE.MeshBasicMaterial({map: texture});
    material.onBeforeCompile = function(shader) {
      fragmentShaderReplacements.forEach((rep) => {
        shader.fragmentShader = shader.fragmentShader.replace(rep.from, rep.to);
      });
      shader.uniforms.paletteTexture = {value: paletteTexture};
      shader.uniforms.indexTexture = {value: indexTexture};
      shader.uniforms.paletteTextureWidth = {value: paletteTextureWidth};
    };
    scene.add(new THREE.Mesh(geometry, material));
  }

  async function loadJSON(url) {
    const req = await fetch(url);
    return req.json();
  }

  let numCountriesSelected = 0;
  let countryInfos;
  async function loadCountryData() {
    countryInfos = await loadJSON('https://threejsfundamentals.org/threejs/resources/data/world/country-info.json');  

    const lonFudge = Math.PI * 1.5;
    const latFudge = Math.PI;
    // these helpers will make it easy to position the boxes
    // We can rotate the lon helper on its Y axis to the longitude
    const lonHelper = new THREE.Object3D();
    // We rotate the latHelper on its X axis to the latitude
    const latHelper = new THREE.Object3D();
    lonHelper.add(latHelper);
    // The position helper moves the object to the edge of the sphere
    const positionHelper = new THREE.Object3D();
    positionHelper.position.z = 1;
    latHelper.add(positionHelper);

    const labelParentElem = document.querySelector('#labels');
    for (const countryInfo of countryInfos) {
      const {lat, lon, min, max, name} = countryInfo;

      // adjust the helpers to point to the latitude and longitude
      lonHelper.rotation.y = THREE.Math.degToRad(lon) + lonFudge;
      latHelper.rotation.x = THREE.Math.degToRad(lat) + latFudge;

      // get the position of the lat/lon
      positionHelper.updateWorldMatrix(true, false);
      const position = new THREE.Vector3();
      positionHelper.getWorldPosition(position);
      countryInfo.position = position;

      // compute the area for each country
      const width = max[0] - min[0];
      const height = max[1] - min[1];
      const area = width * height;
      countryInfo.area = area;

      // add an element for each country
      const elem = document.createElement('div');
      elem.textContent = name;
      labelParentElem.appendChild(elem);
      countryInfo.elem = elem;
    }
    requestRenderIfNotRequested();
  }
  loadCountryData();

  const tempV = new THREE.Vector3();
  const cameraToPoint = new THREE.Vector3();
  const cameraPosition = new THREE.Vector3();
  const normalMatrix = new THREE.Matrix3();

  const settings = {
    minArea: 20,
    maxVisibleDot: -0.2,
  };

  function updateLabels() {
    // exit if we have not loaded the data yet
    if (!countryInfos) {
      return;
    }

    const large = settings.minArea * settings.minArea;
    // get a matrix that represents a relative orientation of the camera
    normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
    // get the camera's position
    camera.getWorldPosition(cameraPosition);
    for (const countryInfo of countryInfos) {
      const {position, elem, area, selected} = countryInfo;
      const largeEnough = area >= large;
      const show = selected || (numCountriesSelected === 0 && largeEnough);
      if (!show) {
        elem.style.display = 'none';
        continue;
      }

      // Orient the position based on the camera's orientation.
      // Since the sphere is at the origin and the sphere is a unit sphere
      // this gives us a camera relative direction vector for the position.
      tempV.copy(position);
      tempV.applyMatrix3(normalMatrix);

      // compute the direction to this position from the camera
      cameraToPoint.copy(position);
      cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize();

      // get the dot product of camera relative direction to this position
      // on the globe with the direction from the camera to that point.
      // -1 = facing directly towards the camera
      // 0 = exactly on tangent of the sphere from the camera
      // > 0 = facing away
      const dot = tempV.dot(cameraToPoint);

      // if the orientation is not facing us hide it.
      if (dot > settings.maxVisibleDot) {
        elem.style.display = 'none';
        continue;
      }

      // restore the element to its default display style
      elem.style.display = '';

      // get the normalized screen coordinate of that position
      // x and y will be in the -1 to +1 range with x = -1 being
      // on the left and y = -1 being on the bottom
      tempV.copy(position);
      tempV.project(camera);

      // convert the normalized position to CSS coordinates
      const x = (tempV.x *  .5 + .5) * canvas.clientWidth;
      const y = (tempV.y * -.5 + .5) * canvas.clientHeight;

      // move the elem to that position
      elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;

      // set the zIndex for sorting
      elem.style.zIndex = (-tempV.z * .5 + .5) * 100000 | 0;
    }
  }

  class GPUPickHelper {
    constructor() {
      // create a 1x1 pixel render target
      this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
      this.pixelBuffer = new Uint8Array(4);
    }
    pick(cssPosition, scene, camera) {
      const {pickingTexture, pixelBuffer} = this;

      // set the view offset to represent just a single pixel under the mouse
      const pixelRatio = renderer.getPixelRatio();
      camera.setViewOffset(
          renderer.getContext().drawingBufferWidth,   // full width
          renderer.getContext().drawingBufferHeight,  // full top
          cssPosition.x * pixelRatio | 0,             // rect x
          cssPosition.y * pixelRatio | 0,             // rect y
          1,                                          // rect width
          1,                                          // rect height
      );
      // render the scene
      renderer.setRenderTarget(pickingTexture);
      renderer.render(scene, camera);
      renderer.setRenderTarget(null);
      // clear the view offset so rendering returns to normal
      camera.clearViewOffset();
      //read the pixel
      renderer.readRenderTargetPixels(
          pickingTexture,
          0,   // x
          0,   // y
          1,   // width
          1,   // height
          pixelBuffer);

      const id =
          (pixelBuffer[0] <<  0) |
          (pixelBuffer[1] <<  8) |
          (pixelBuffer[2] << 16);

      return id;
    }
  }

  const pickHelper = new GPUPickHelper();

  function getCanvasRelativePosition(event) {
    const rect = canvas.getBoundingClientRect();
    return {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    };
  }

  function pickCountry(event) {
    // exit if we have not loaded the data yet
    if (!countryInfos) {
      return;
    }

    const position = getCanvasRelativePosition(event);
    const id = pickHelper.pick(position, pickingScene, camera);
    if (id > 0) {
      const countryInfo = countryInfos[id - 1];
      const selected = !countryInfo.selected;
      if (selected && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
        unselectAllCountries();
      }
      numCountriesSelected += selected ? 1 : -1;
      countryInfo.selected = selected;
    } else if (numCountriesSelected) {
      unselectAllCountries();
    }
    requestRenderIfNotRequested();
  }

  function unselectAllCountries() {
    numCountriesSelected = 0;
    countryInfos.forEach((countryInfo) => {
      countryInfo.selected = false;
    });
  }

  canvas.addEventListener('mouseup', pickCountry);

  let lastTouch;
  canvas.addEventListener('touchstart', (event) => {
    // prevent the window from scrolling
    event.preventDefault();
    lastTouch = event.touches[0];
  }, {passive: false});
  canvas.addEventListener('touchmove', (event) => {
    lastTouch = event.touches[0];
  });
  canvas.addEventListener('touchend', () => {
    pickCountry(lastTouch);
  });

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  let renderRequested = false;

  function render() {
    renderRequested = undefined;

    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement;
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    controls.update();

    updateLabels();

    renderer.render(scene, camera);
  }
  render();

  function requestRenderIfNotRequested() {
    if (!renderRequested) {
      renderRequested = true;
      requestAnimationFrame(render);
    }
  }

  controls.addEventListener('change', requestRenderIfNotRequested);
  window.addEventListener('resize', requestRenderIfNotRequested);
}

main();