Skip to content

Commit

Permalink
Add HTML elements layer and example
Browse files Browse the repository at this point in the history
  • Loading branch information
vasturiano committed Mar 4, 2022
1 parent aae83ce commit 75fb074
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 4 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,25 @@ Check out the examples:
* [Ripple Rings](https://vasturiano.github.io/three-globe/example/ripples/) ([source](https://github.com/vasturiano/three-globe/blob/master/example/ripples/index.html))
* [Solar Terminator](https://vasturiano.github.io/three-globe/example/solar-terminator/) ([source](https://github.com/vasturiano/three-globe/blob/master/example/solar-terminator/index.html))
* [Labels](https://vasturiano.github.io/three-globe/example/labels/) ([source](https://github.com/vasturiano/three-globe/blob/master/example/labels/index.html))
* [HTML Markers](https://vasturiano.github.io/three-globe/example/htmlMarkers/) ([source](https://github.com/vasturiano/three-globe/blob/master/example/htmlMarkers/index.html))
* [Satellites](https://vasturiano.github.io/three-globe/example/satellites/) ([source](https://github.com/vasturiano/three-globe/blob/master/example/satellites/index.html))
* [Custom Globe Material](https://vasturiano.github.io/three-globe/example/custom-material/) ([source](https://github.com/vasturiano/three-globe/blob/master/example/custom-material/index.html))
* [Custom Layer](https://vasturiano.github.io/three-globe/example/custom/) ([source](https://github.com/vasturiano/three-globe/blob/master/example/custom/index.html))

Available Map Layers:
* [Globe Layer](#globe-layer)
* [Points Layer](#points-layer)
* [Arcs Layer](#arcs-layer)
* [Polygons Layer](#polygons-layer)
* [Paths Layer](#paths-layer)
* [Hex Bin Layer](#hex-bin-layer)
* [Hexed Polygons Layer](#hexed-polygons-layer)
* [Tiles Layer](#tiles-layer)
* [Rings Layer](#rings-layer)
* [HTML Elements Layer](#html-elements-layer)
* [3D Objects Layer](#3d-objects-layer)
* [Custom Layer](#custom-layer)

## Quick start

```js
Expand Down Expand Up @@ -223,7 +238,18 @@ new ThreeGlobe({ configOptions })
| <b>labelDotOrientation</b>([<i>str</i> or <i>fn</i>]) | Label object accessor function or attribute for the orientation of the label if the dot marker is present. Possible values are `right`, `top` and `bottom`. | `() => 'bottom'` |
| <b>labelsTransitionDuration</b>([<i>num</i>]) | Getter/setter for duration (ms) of the transition to animate label changes involving position modifications (`lat`, `lng`, `altitude`, `rotation`). A value of `0` will move the labels immediately to their final position. New labels are animated by scaling their size. | 1000 |

### Objects Layer
### HTML Elements Layer

| Method | Description | Default |
| --- | --- | :--: |
| <b>htmlElementsData</b>([<i>array</i>]) | Getter/setter for the list of objects to represent in the HTML elements map layer. Each HTML element is rendered using [ThreeJS CSS2DRenderer](https://threejs.org/docs/#examples/en/renderers/CSS2DRenderer). | `[]` |
| <b>htmlLat</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | HTML element accessor function, attribute or a numeric constant for the latitude coordinate of the element's central position. | `lat` |
| <b>htmlLng</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | HTML element accessor function, attribute or a numeric constant for the longitude coordinate of the element's central position. | `lng` |
| <b>htmlAltitude</b>([<i>num</i>, <i>str</i> or <i>fn</i>]) | HTML element accessor function, attribute or a numeric constant for the altitude coordinate of the element's position, in terms of globe radius units. | 0 |
| <b>htmlElement</b>([<i>str</i> or <i>fn</i>]) | Accessor function or attribute to retrieve the DOM element to use. Should return an instance of [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). | `null` |
| <b>htmlTransitionDuration</b>([<i>num</i>]) | Getter/setter for duration (ms) of the transition to animate HTML elements position changes. A value of `0` will move the elements immediately to their final position. | 1000 |

### 3D Objects Layer

| Method | Description | Default |
| --- | --- | :--: |
Expand Down
85 changes: 85 additions & 0 deletions example/htmlMarkers/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<head>
<style> body { margin: 0; } </style>

<script src="//unpkg.com/three"></script>
<script src="//unpkg.com/three/examples/js/controls/TrackballControls.js"></script>
<script src="//unpkg.com/three/examples/js/renderers/CSS2DRenderer.js"></script>

<script src="//unpkg.com/three-globe"></script>
<!-- <script src="../../dist/three-globe.js"></script>-->
</head>

<body>
<div id="globeViz"></div>

<script>
const markerSvg = `<svg viewBox="-4 0 36 36">
<path fill="currentColor" d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z"></path>
<circle fill="black" cx="14" cy="14" r="7"></circle>
</svg>`;

// Gen random data
const N = 30;
const gData = [...Array(N).keys()].map(() => ({
lat: (Math.random() - 0.5) * 180,
lng: (Math.random() - 0.5) * 360,
size: 7 + Math.random() * 30,
color: ['red', 'white', 'blue', 'green'][Math.round(Math.random() * 3)]
}));

const Globe = new ThreeGlobe()
.globeImageUrl('//unpkg.com/three-globe/example/img/earth-dark.jpg')
.bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png')
.htmlElementsData(gData)
.htmlElement(d => {
const el = document.createElement('div');
el.innerHTML = markerSvg;
el.style.color = d.color;
el.style.width = `${d.size}px`;
return el;
});

// Setup renderers
const renderers = [new THREE.WebGLRenderer(), new THREE.CSS2DRenderer()];
renderers.forEach((r, idx) => {
r.setSize(window.innerWidth, window.innerHeight);
if (idx > 0) {
// overlay additional on top of main renderer
r.domElement.style.position = 'absolute';
r.domElement.style.top = '0px';
r.domElement.style.pointerEvents = 'none';
}
document.getElementById('globeViz').appendChild(r.domElement);
});

// Setup scene
const scene = new THREE.Scene();
scene.add(Globe);
scene.add(new THREE.AmbientLight(0xbbbbbb));
scene.add(new THREE.DirectionalLight(0xffffff, 0.6));

// Setup camera
const camera = new THREE.PerspectiveCamera();
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
camera.position.z = 500;

// Add camera controls
const tbControls = new THREE.TrackballControls(camera, renderers[0].domElement);
tbControls.minDistance = 101;
tbControls.rotateSpeed = 5;
tbControls.zoomSpeed = 0.8;

// Update pov when camera moves
Globe.setPointOfView(camera.position, Globe.position);
tbControls.addEventListener('change', () => Globe.setPointOfView(camera.position, Globe.position));

// Kick-off renderers
(function animate() { // IIFE
// Frame cycle
tbControls.update();
renderers.forEach(r => r.render(scene, camera));
requestAnimationFrame(animate);
})();
</script>
</body>
52 changes: 50 additions & 2 deletions src/globe-kapsule.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import PathsLayerKapsule from './layers/paths';
import TilesLayerKapsule from './layers/tiles';
import LabelsLayerKapsule from './layers/labels';
import RingsLayerKapsule from './layers/rings';
import HtmlElementsLayerKapsule from './layers/htmlElements';
import ObjectsLayerKapsule from './layers/objects';
import CustomLayerKapsule from './layers/custom';

Expand All @@ -45,6 +46,7 @@ const layers = [
'tilesLayer',
'labelsLayer',
'ringsLayer',
'htmlElementsLayer',
'objectsLayer',
'customLayer'
];
Expand Down Expand Up @@ -202,6 +204,16 @@ const linkedRingsLayerProps = Object.assign(...[
'ringRepeatPeriod'
].map(p => ({ [p]: bindRingsLayer.linkProp(p)})));

const bindHtmlElementsLayer = linkKapsule('htmlElementsLayer', HtmlElementsLayerKapsule);
const linkedHtmlElementsLayerProps = Object.assign(...[
'htmlElementsData',
'htmlLat',
'htmlLng',
'htmlAltitude',
'htmlElement',
'htmlTransitionDuration'
].map(p => ({ [p]: bindHtmlElementsLayer.linkProp(p)})));

const bindObjectsLayer = linkKapsule('objectsLayer', ObjectsLayerKapsule);
const linkedObjectsLayerProps = Object.assign(...[
'objectsData',
Expand Down Expand Up @@ -240,6 +252,7 @@ export default Kapsule({
...linkedTilesLayerProps,
...linkedLabelsLayerProps,
...linkedRingsLayerProps,
...linkedHtmlElementsLayerProps,
...linkedObjectsLayerProps,
...linkedCustomLayerProps
},
Expand All @@ -248,11 +261,40 @@ export default Kapsule({
getGlobeRadius,
getCoords: (state, ...args) => polar2Cartesian(...args),
toGeoCoords: (state, ...args) => cartesian2Polar(...args),
setPointOfView: (state, globalPov, globePos) => {
let isBehindGlobe = undefined;
if (globalPov) {
const globeRadius = getGlobeRadius();
const pov = globePos ? globalPov.clone().sub(globePos) : globalPov; // convert to local vector

let povDist, povEdgeDist, povEdgeAngle, maxSurfacePosAngle;
isBehindGlobe = pos => {
povDist === undefined && (povDist = pov.length());

// check if it's behind plane of globe's visible area
// maxSurfacePosAngle === undefined && (maxSurfacePosAngle = Math.acos(globeRadius / povDist));
// return pov.angleTo(pos) > maxSurfacePosAngle;

// more sophisticated method that checks also pos altitude
povEdgeDist === undefined && (povEdgeDist = Math.sqrt(povDist**2 - globeRadius**2));
povEdgeAngle === undefined && (povEdgeAngle = Math.acos(povEdgeDist / povDist));
const povPosDist = pov.distanceTo(pos);
if (povPosDist < povEdgeDist) return false; // pos is closer than visible edge of globe

const posDist = pos.length();
const povPosAngle = Math.acos((povDist**2 + povPosDist**2 - posDist**2) / (2 * povDist * povPosDist)); // triangle solver
return povPosAngle < povEdgeAngle; // pos is within globe's visible area cone
};
}

// pass behind globe checker for layers that need it
state.layersThatNeedBehindGlobeChecker.forEach(l => l.isBehindGlobe(isBehindGlobe));
},
...linkedGlobeLayerMethods
},

stateInit: () => {
return {
const layers = {
globeLayer: GlobeLayerKapsule(),
pointsLayer: PointsLayerKapsule(),
arcsLayer: ArcsLayerKapsule(),
Expand All @@ -263,9 +305,15 @@ export default Kapsule({
tilesLayer: TilesLayerKapsule(),
labelsLayer: LabelsLayerKapsule(),
ringsLayer: RingsLayerKapsule(),
htmlElementsLayer: HtmlElementsLayerKapsule(),
objectsLayer: ObjectsLayerKapsule(),
customLayer: CustomLayerKapsule()
}
};

return {
...layers,
layersThatNeedBehindGlobeChecker: Object.values(layers).filter(l => l.hasOwnProperty('isBehindGlobe'))
};
},

init(threeObj, state, { animateIn = true, waitForGlobeReady = true }) {
Expand Down
17 changes: 16 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Object3D, Vector2, Material } from 'three';
import { Object3D, Vector2, Vector3, Material } from 'three';

type Accessor<In, Out> = Out | string | ((obj: In) => Out);
type ObjAccessor<T> = Accessor<object, T>;
Expand Down Expand Up @@ -261,6 +261,20 @@ export declare class ThreeGlobeGeneric<ChainableInstance> extends Object3D {
ringRepeatPeriod(): ObjAccessor<number>;
ringRepeatPeriod(msAccessor: ObjAccessor<number>): ChainableInstance;

// HTML Elements layer
htmlElementsData(): object[];
htmlElementsData(data: object[]): ChainableInstance;
htmlLat(): ObjAccessor<number>;
htmlLat(latitudeAccessor: ObjAccessor<number>): ChainableInstance;
htmlLng(): ObjAccessor<number>;
htmlLng(longitudeAccessor: ObjAccessor<number>): ChainableInstance;
htmlAltitude(): ObjAccessor<number>;
htmlAltitude(altitudeAccessor: ObjAccessor<number>): ChainableInstance;
htmlElement(): HTMLElement | string | ((d: object) => HTMLElement);
htmlElement(htmlElementAccessor: HTMLElement | string | ((d: object) => HTMLElement)): ChainableInstance;
htmlTransitionDuration(): number;
htmlTransitionDuration(durationMs: number): ChainableInstance;

// Objects layer
objectsData(): object[];
objectsData(data: object[]): ChainableInstance;
Expand All @@ -285,6 +299,7 @@ export declare class ThreeGlobeGeneric<ChainableInstance> extends Object3D {
getGlobeRadius(): number;
getCoords(lat: number, lng: number, altitude?: number): { x: number, y: number, z: number };
toGeoCoords(coords: { x: number, y: number, z: number }): { lat: number, lng: number, altitude: number };
setPointOfView(pov: Vector3, globePos?: Vector3): void;

// Render options
rendererSize(): Vector2;
Expand Down
95 changes: 95 additions & 0 deletions src/layers/htmlElements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

const THREE = {
...(window.THREE
? window.THREE // Prefer consumption from global THREE, if exists
: {}
),
CSS2DObject
};

import Kapsule from 'kapsule';
import accessorFn from 'accessor-fn';
import TWEEN from '@tweenjs/tween.js';

import { emptyObject } from '../utils/gc';
import threeDigest from '../utils/digest';
import { polar2Cartesian } from '../utils/coordTranslate';

//

export default Kapsule({
props: {
htmlElementsData: { default: [] },
htmlLat: { default: 'lat' },
htmlLng: { default: 'lng' },
htmlAltitude: { default: 0 }, // in units of globe radius
htmlElement: {},
htmlTransitionDuration: { default: 1000, triggerUpdate: false }, // ms
isBehindGlobe: { onChange() { this.updateObjVisibility() }, triggerUpdate: false }
},

methods: {
updateObjVisibility(state, obj) {
// default to all if no obj specified
const objs = obj ? [obj] : state.htmlElementsData.map(d => d.__threeObj).filter(d => d);
// Hide elements on the far side of the globe
objs.forEach(obj => (obj.visible = !state.isBehindGlobe || !state.isBehindGlobe(obj.position)));
}
},

init(threeObj, state) {
// Clear the scene
emptyObject(threeObj);

// Main three object to manipulate
state.scene = threeObj;
},

update(state, changedProps) {
// Data accessors
const latAccessor = accessorFn(state.htmlLat);
const lngAccessor = accessorFn(state.htmlLng);
const altitudeAccessor = accessorFn(state.htmlAltitude);
const elemAccessor = accessorFn(state.htmlElement);

threeDigest(state.htmlElementsData, state.scene, {
// objs need to be recreated if this prop has changed
purge: changedProps.hasOwnProperty('htmlElement'),
createObj: d => {
let elem = elemAccessor(d);

const obj = new THREE.CSS2DObject(elem);

obj.__globeObjType = 'html'; // Add object type

return obj;
},
updateObj: (obj, d) => {
const applyUpdate = td => {
const { alt, lat, lng } = obj.__currentTargetD = td;
Object.assign(obj.position, polar2Cartesian(lat, lng, alt));
this.updateObjVisibility(obj);
};

const targetD = {
lat: +latAccessor(d),
lng: +lngAccessor(d),
alt: +altitudeAccessor(d)
};

if (!state.htmlTransitionDuration || state.htmlTransitionDuration < 0 || !obj.__currentTargetD) {
// set final position
applyUpdate(targetD);
} else {
// animate
new TWEEN.Tween(obj.__currentTargetD)
.to(targetD, state.pointsTransitionDuration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(applyUpdate)
.start();
}
}
});
}
});

0 comments on commit 75fb074

Please sign in to comment.