簡単な地図を自作する(HTML/CSS/JS)
前書き
three.jsを使用して、簡単な地図を作る方法を掲載しています。
目次
- 地図を作る
- 参考
地図を作る
イラレで、各SVG画像のサイズ感を合わせて、書き出す
スタイルはインラインに記述されるようにする(iOS対策 2022.09)
three.jsとGSAPを読み込む。
three.js
GSAP Installation
SVG画像から、メッシュを作成、配置(SVGLoader.js)
レイキャストを設定(ある点から発射された光線を追跡し, 物体との衝突を検出する)
アイコンにマウスを乗せたらカーソルを変更する
アイコンをクリックしたらイベント処理を行う
カメラの動作を制御(OrbitControls.js)
水平方向への移動 | 拡大縮小 | |
---|---|---|
マウス | 左ボタンを押しながら移動 | ホイールを動かす |
スマホ | 2本の指を移動 | 2本の指を広げる、狭める |
キーボード | 左、右、上、下キー | |
HTMLのボタン | 左、右、上、下ボタン 場所のボタン |
+、-ボタン |
<script defer src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script defer type="importmap">
{
"imports": {
"three": "./three_r142/build/three.module.js"
}
}
</script>
<script defer type="module" src="名前.js"></script>
<div class="wrap js-wrap-a">
<canvas class="canvas js-canvas-a"></canvas>
<div class="op-zoom">
<div class="op-icon js-plus">+</div>
<div class="op-icon js-minus">-</div>
</div>
<div class="op-icon op-left js-left">◀</div>
<div class="op-icon op-right js-right">▶</div>
<div class="op-icon op-up js-up">▲</div>
<div class="op-icon op-down js-down">▼</div>
<img class="op-houi" src="<?= PATH; ?>assets/img/45/houi.svg" alt="">
</div>
<div class="nav-wrap">
<div class="nav" data-pagenav="jingu">伊勢神宮</div>
<div class="nav" data-pagenav="fujisan">富士山</div>
<div class="nav" data-pagenav="skytree">スカイツリー</div>
<div class="nav" data-pagenav="tanegasima">種子島</div>
</div>
.wrap{
position: relative; padding: 75% 0 0; overflow: hidden; cursor: grab;
}
.canvas{
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
.op-icon{
width: 1.75em; height: 1.75em; line-height: 1.75; color: #fff; font-weight: bold; text-align: center; background: #947343; border-radius: 3px; cursor: pointer;
}
.op-zoom{ position: absolute; right: 10px; bottom: 10px; display: flex; gap: 0 5px; }
.op-left{ position: absolute; top: 0; bottom: 0; left: 10px; margin: auto; }
.op-right{ position: absolute; top: 0; right: 10px; bottom: 0; margin: auto; }
.op-up{ position: absolute; top: 10px; right: 0; left: 0; margin: auto; }
.op-down{ position: absolute; right: 0; bottom: 10px; left: 0; margin: auto; }
.op-houi{ position: absolute; top: 10px; right: 10px;width: 2em; }
.nav-wrap{
display: flex; flex-wrap: wrap; justify-content: center; gap: 1em 1em; margin: 20px 0 0;
}
.nav{
width: 8em; color: #fff; font-weight: bold; text-align: center; background-color: #947343; border-radius: 5px; cursor: pointer;
}
import * as THREE from 'three';
import { OrbitControls } from './three_r142/examples/jsm/controls/OrbitControls.js';
import { SVGLoader } from './three_r142/examples/jsm/loaders/SVGLoader.js';
import { gsap } from './gsap_3.10.4/esm/gsap-core.js';
// サイズ------------------
const wrap = document.querySelector(".js-wrap-a");
let wrapWidth = wrap.clientWidth;
let wrapHeight = wrap.clientHeight;
// レンダラー------------------
const canvas = document.querySelector(".js-canvas-a");
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(wrapWidth, wrapHeight);
// シーン------------------
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf7f1d8 );
// カメラ------------------
const camera = new THREE.OrthographicCamera( wrapWidth / - 2, wrapWidth / 2, wrapHeight / 2, wrapHeight / - 2, );
camera.position.set(0, 0, 100);
// カメラコントローラー
const controls = new OrbitControls( camera, renderer.domElement );
controls.enableRotate = false; //回転させない
controls.minZoom = 0.75; //最小縮小率
controls.maxZoom = 5; //最大拡大率
controls.listenToKeyEvents( window ); //windowにイベントリスナーを追加
controls.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' } //キーボード
controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.DOLLY } //マウス
controls.touches = { TWO: THREE.TOUCH.DOLLY_PAN } //タッチ
// 物体 ------------------
// 準備
const imgPath = "パス";
const mapX = -300;
const mapY = 380;
//イラレの情報を元に、位置、大きさを指定
const svglist = [
{ ID: "nihon",EVENT: false, IMG: "tizu.svg", X: mapX, Y: mapY, Z: 1, WIDTH: 525.3, HEIGHT: 687.8 },
{ ID: "jingu", EVENT: true, IMG: "torii.svg", X: mapX + 329, Y: mapY - 411, Z: 2, WIDTH: 23.7, HEIGHT: 20.2 },
{ ID: "fujisan", EVENT: true, IMG: "yama.svg", X: mapX + 361, Y: mapY - 390, Z: 2, WIDTH: 31.8, HEIGHT: 20.2 },
{ ID: "skytree", EVENT: true, IMG: "tou.svg", X: mapX + 392.5, Y: mapY - 373, Z: 2, WIDTH: 9.8, HEIGHT: 30.4 },
{ ID: "tanegasima", EVENT: true, IMG: "rocket.svg", X: mapX + 223.5, Y: mapY - 489, Z: 2, WIDTH: 19, HEIGHT: 28.1 },
];
// svgからメッシュを作成
// グループを作成
const itemGroup = new THREE.Group();
// SVGの取り込み
const loadSVG = (item) => {
const shapeGroup = new THREE.Group();
const loader = new SVGLoader();
loader.load( imgPath + item.IMG, (data) => {
// シェイプを作成
const svgPaths = data.paths;
for ( let i = 0; i < svgPaths.length; i ++ ) {
const fillColor = svgPaths[i].userData.style.fill;
const svgShape = SVGLoader.createShapes(svgPaths[i]);
for ( let j = 0; j < svgShape.length; j ++ ) {
// メッシュ
const geometry = new THREE.ShapeGeometry(svgShape[j]);
const material = new THREE.MeshBasicMaterial( {
color: new THREE.Color(fillColor),
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
// 設定
mesh.userData.ID = item.ID;
mesh.userData.EVENT = item.EVENT;
// グループに追加
shapeGroup.add( mesh );
}
}
// 位置、向き
shapeGroup.position.set(item.X, item.Y, item.Z);
shapeGroup.rotation.x = Math.PI;
// 設定
shapeGroup.userData.EVENT = item.EVENT;
shapeGroup.userData.WIDTH = item.WIDTH;
shapeGroup.userData.HEIGHT = item.HEIGHT;
shapeGroup.userData.X = item.X;
shapeGroup.userData.Y = item.Y;
// グループに追加
itemGroup.add( shapeGroup );
});
}
// 配列の数だけ繰り返す
for ( let i = 0; i < svglist.length; i ++ ) {
loadSVG(svglist[i]);
}
// シーンに追加
scene.add(itemGroup);
//レイキャスト(ある点から発射された光線を追跡し, 物体との衝突を検出する)------------------
const mouse = new THREE.Vector2(); // マウス座標管理用のベクトル
let intersects;
//レイキャストを生成
const raycaster = new THREE.Raycaster();
const handleRaycaster = () => {
raycaster.setFromCamera(mouse, camera); // マウス位置からまっすぐに伸びる光線ベクトルを生成
intersects = raycaster.intersectObjects(scene.children); // その光線とぶつかったオブジェクトを得る
}
//マウスの座標を変更
const handleMouseMove = (e) => {
const wrapReact = wrap.getBoundingClientRect();
const x = e.clientX - wrapReact.left;
const y = e.clientY - wrapReact.top;
mouse.x = (x / wrapWidth) * 2 - 1;
mouse.y = - (y / wrapHeight) * 2 + 1;
}
canvas.addEventListener('mousemove', handleMouseMove);
//カーソルの切替
const handleCursor = () => {
if(intersects.length > 0){ //レイキャストとぶつかった物体があれば
if( intersects[0].object.userData.EVENT ){ //一番手前の物体
wrap.style.cursor = 'pointer';
}else{
wrap.style.cursor = 'grab';
}
}else{
wrap.style.cursor = 'grab';
}
}
//場所のアイコンをクリックしたらアラートを出す
const itemClick = () => {
if (intersects.length > 0) { //レイキャストとぶつかった物体があれば
const id = intersects[0].object.userData.ID; //一番手前の物体
if( intersects[0].object.userData.EVENT ){
if(id == "jingu"){
alert("伊勢神宮");
}else if(id == "fujisan"){
alert("富士山");
}else if(id == "skytree"){
alert("東京スカイツリー");
}else if(id == "tanegasima"){
alert("種子島宇宙センター");
}
}
}
}
canvas.addEventListener('click',itemClick);
//カメラの拡大倍率に合わせて、場所のアイコンを縮小する
const itemZoom = () => {
const zoom = controls.object.zoom
const shrink = 1 / zoom;
for ( let i = 0; i < itemGroup.children.length; i ++ ) {
const item = itemGroup.children[i];
if( item.userData.EVENT ){
//縮小
item.scale.x = shrink;
item.scale.y = shrink;
//中央寄せ
item.position.x = item.userData.X + item.userData.WIDTH / 2 * (1 - shrink);
item.position.y = item.userData.Y - item.userData.HEIGHT / 2 * (1 - shrink);
}
}
}
controls.addEventListener("change",itemZoom);
//拡大縮小ボタンをクリック
const btnPlus = document.querySelector(".js-plus");
const btnMinus = document.querySelector(".js-minus");
const mapZoom = (btn, direction) => {
let interval;
btn.addEventListener('pointerdown', () => {
interval = setInterval( () => {
if( direction == "plus" ){
controls.object.zoom = Math.min( controls.maxZoom, controls.object.zoom * 1.1 ); //最大値を上回らないようにする
}else if( direction == "minus" ){
controls.object.zoom = Math.max( controls.minZoom, controls.object.zoom * 0.9 ); //最小値を下回らないようにする
}
itemZoom();
}, 50);
});
btn.addEventListener('pointerup', () => clearInterval(interval) );
btn.addEventListener('pointerleave', () => clearInterval(interval) );
}
mapZoom(btnPlus, "plus");
mapZoom(btnMinus, "minus");
//上下左右ボタンをクリック
const btnLeft = document.querySelector(".js-left");
const btnRight = document.querySelector(".js-right");
const btnUp = document.querySelector(".js-up");
const btnDown = document.querySelector(".js-down");
const mapMove = (btn, direction) => {
const distance = controls.keyPanSpeed;
let interval;
btn.addEventListener('pointerdown', () => {
interval = setInterval( () => {
//カメラの位置と視点を動かす
if( direction == "left" ){
controls.object.position.x -= distance;
controls.target.x -= distance;
}else if( direction == "right" ){
controls.object.position.x += distance;
controls.target.x += distance;
}else if( direction == "up" ){
controls.object.position.y += distance;
controls.target.y += distance;
}else if( direction == "down" ){
controls.object.position.y -= distance;
controls.target.y -= distance;
}
}, 50);
});
btn.addEventListener('pointerup', () => clearInterval(interval) );
btn.addEventListener('pointerleave', () => clearInterval(interval) );
}
mapMove(btnLeft, "left");
mapMove(btnRight, "right");
mapMove(btnUp, "up");
mapMove(btnDown, "down");
//ナビ------------------
document.querySelectorAll('[data-mapnav]').forEach((item) => {
item.addEventListener('click', () => {
const id = item.dataset.mapnav;
for ( let i = 0; i < svglist.length; i ++ ) {
if( svglist[i].ID == id){
const pointX = svglist[i].X + svglist[i].WIDTH / 2;
const pointY = svglist[i].Y - svglist[i].HEIGHT / 2;
gsap.to( controls.object.position, { x: pointX });
gsap.to( controls.target, { x: pointX });
gsap.to( controls.object.position, { y: pointY });
gsap.to( controls.target, { y: pointY });
}
}
});
});
//リサイズ------------------
const wrapResize = () =>{
wrapWidth = wrap.clientWidth;
wrapHeight = wrap.clientHeight;
renderer.setSize(wrapWidth, wrapHeight);
// camera.aspect = wrapWidth / wrapHeight;
camera.updateProjectionMatrix();
}
//一定時間毎に処理------------------
let tick;
const switching = (e) => {
if( e[0].isIntersecting ){ //見えてる時
tick = () => {
wrapResize(); //リサイズ
controls.update(); //カメラコントローラー
handleRaycaster(); //レイキャスト
handleCursor(); //カーソル
renderer.render(scene, camera); //レンダリング
requestAnimationFrame(tick); //繰り返し
}
requestAnimationFrame(tick);
}else{ //見えてない時
tick = () => {
cancelAnimationFrame(tick);
}
}
}
//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
let observer;
const options = { root: null, rootMargin: "0%", threshold: 0 };
observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
observer.observe(wrap); //要素の監視
}
createObserver();
参考
Three.jsでオブジェクトとの交差を調べる - ICS MEDIA
関連記事
マウスの位置に合わせて要素を動かす(HTML/CSS/JS)
この記事は役に立ちましたか?
送信中