From 75fb07404926b207d136628ca2b0c16723423eb5 Mon Sep 17 00:00:00 2001 From: vasco Date: Fri, 4 Mar 2022 01:39:41 +0000 Subject: [PATCH] Add HTML elements layer and example --- README.md | 28 +++++++++- example/htmlMarkers/index.html | 85 ++++++++++++++++++++++++++++++ src/globe-kapsule.js | 52 ++++++++++++++++++- src/index.d.ts | 17 +++++- src/layers/htmlElements.js | 95 ++++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 4 deletions(-) create mode 100755 example/htmlMarkers/index.html create mode 100755 src/layers/htmlElements.js diff --git a/README.md b/README.md index 19cad96..49f2947 100755 --- a/README.md +++ b/README.md @@ -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 @@ -223,7 +238,18 @@ new ThreeGlobe({ configOptions }) | labelDotOrientation([str or fn]) | 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'` | | labelsTransitionDuration([num]) | 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 | +| --- | --- | :--: | +| htmlElementsData([array]) | 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). | `[]` | +| htmlLat([num, str or fn]) | HTML element accessor function, attribute or a numeric constant for the latitude coordinate of the element's central position. | `lat` | +| htmlLng([num, str or fn]) | HTML element accessor function, attribute or a numeric constant for the longitude coordinate of the element's central position. | `lng` | +| htmlAltitude([num, str or fn]) | 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 | +| htmlElement([str or fn]) | 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` | +| htmlTransitionDuration([num]) | 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 | | --- | --- | :--: | diff --git a/example/htmlMarkers/index.html b/example/htmlMarkers/index.html new file mode 100755 index 0000000..a56f457 --- /dev/null +++ b/example/htmlMarkers/index.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/globe-kapsule.js b/src/globe-kapsule.js index 0b4f1a8..9af189f 100755 --- a/src/globe-kapsule.js +++ b/src/globe-kapsule.js @@ -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'; @@ -45,6 +46,7 @@ const layers = [ 'tilesLayer', 'labelsLayer', 'ringsLayer', + 'htmlElementsLayer', 'objectsLayer', 'customLayer' ]; @@ -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', @@ -240,6 +252,7 @@ export default Kapsule({ ...linkedTilesLayerProps, ...linkedLabelsLayerProps, ...linkedRingsLayerProps, + ...linkedHtmlElementsLayerProps, ...linkedObjectsLayerProps, ...linkedCustomLayerProps }, @@ -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(), @@ -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 }) { diff --git a/src/index.d.ts b/src/index.d.ts index 21dcd07..dc99d9d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,4 +1,4 @@ -import { Object3D, Vector2, Material } from 'three'; +import { Object3D, Vector2, Vector3, Material } from 'three'; type Accessor = Out | string | ((obj: In) => Out); type ObjAccessor = Accessor; @@ -261,6 +261,20 @@ export declare class ThreeGlobeGeneric extends Object3D { ringRepeatPeriod(): ObjAccessor; ringRepeatPeriod(msAccessor: ObjAccessor): ChainableInstance; + // HTML Elements layer + htmlElementsData(): object[]; + htmlElementsData(data: object[]): ChainableInstance; + htmlLat(): ObjAccessor; + htmlLat(latitudeAccessor: ObjAccessor): ChainableInstance; + htmlLng(): ObjAccessor; + htmlLng(longitudeAccessor: ObjAccessor): ChainableInstance; + htmlAltitude(): ObjAccessor; + htmlAltitude(altitudeAccessor: ObjAccessor): 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; @@ -285,6 +299,7 @@ export declare class ThreeGlobeGeneric 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; diff --git a/src/layers/htmlElements.js b/src/layers/htmlElements.js new file mode 100755 index 0000000..1924a48 --- /dev/null +++ b/src/layers/htmlElements.js @@ -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(); + } + } + }); + } +});