331 lines
8.7 KiB
TypeScript
331 lines
8.7 KiB
TypeScript
// @ts-nocheck
|
|
import { Renderer, Camera, Vec3, Orbit, Sphere, Transform, Program, Mesh, Texture } from 'ogl'
|
|
// Shaders
|
|
import VERTEX_SHADER from '../../modules/globe2/vertex.glsl?raw'
|
|
import FRAGMENT_SHADER from '../../modules/globe2/frag.glsl?raw'
|
|
|
|
|
|
export class Globe {
|
|
constructor (options: Options) {
|
|
// Options
|
|
this.options = options
|
|
this.el = options.el
|
|
this.parent = options.parent
|
|
this.width = this.el.offsetWidth
|
|
this.height = this.el.offsetHeight
|
|
this.markers = options.markers || []
|
|
|
|
// Parameters
|
|
this.params = {
|
|
autoRotate: options.autoRotate,
|
|
speed: options.speed,
|
|
}
|
|
|
|
// Misc
|
|
this.hoveringMarker = false
|
|
this.dragging = false
|
|
this.webgl = WebGLSupport() !== null
|
|
this.pane = undefined
|
|
|
|
// Run globe after check for WebGL support
|
|
if (this.webgl) {
|
|
this.build()
|
|
this.resize()
|
|
this.render()
|
|
}
|
|
|
|
// Add GUI panel if activated
|
|
if (this.options.pane) {
|
|
import('./pane').then(({ createPane }) => {
|
|
createPane(this)
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Build scene
|
|
*/
|
|
build () {
|
|
// Create renderer
|
|
this.renderer = new Renderer({
|
|
dpr: this.options.dpr || 1,
|
|
alpha: true,
|
|
premultiplyAlpha: false,
|
|
antialias: this.options.antialias || true,
|
|
})
|
|
this.gl = this.renderer.gl
|
|
|
|
// Create camera
|
|
this.camera = new Camera(this.gl)
|
|
// TODO: Why 1.315? Is there a way to calculate this number?
|
|
this.camera.position.set(0, 0, 1.315)
|
|
|
|
// Create controls
|
|
this.controls = new Orbit(this.camera, {
|
|
element: this.el,
|
|
target: new Vec3(0,0,0),
|
|
enableZoom: false,
|
|
enablePan: false,
|
|
autoRotate: false,
|
|
ease: 0.2,
|
|
minPolarAngle: Math.PI / 4,
|
|
maxPolarAngle: Math.PI / 1.5,
|
|
})
|
|
|
|
// Append canvas to scene
|
|
this.el.appendChild(this.gl.canvas)
|
|
|
|
// Create scene and geometry
|
|
this.scene = new Transform()
|
|
this.geometry = new Sphere(this.gl, {
|
|
widthSegments: 64,
|
|
heightSegments: 64,
|
|
})
|
|
|
|
// Create light
|
|
// TODO: How to create a nicer light that doesn't fade to 0? Just creating a "dark" area where you still can read markers and see countries/continents
|
|
// this.light = new Vec3(0, 50, 150)
|
|
this.light = new Vec3(0, 0, 15)
|
|
|
|
// Add map texture
|
|
const map = new Texture(this.gl)
|
|
const img = new Image()
|
|
img.onload = () => (map.image = img)
|
|
img.src = this.options.mapFile
|
|
|
|
// Create program
|
|
this.program = new Program(this.gl, {
|
|
vertex: VERTEX_SHADER,
|
|
fragment: FRAGMENT_SHADER,
|
|
uniforms: {
|
|
u_dt: { value: 0 },
|
|
u_lightWorldPosition: { value: this.light }, // Position of the Light
|
|
u_shininess: { value: 1.0 },
|
|
map: { value: map }, // Color Map
|
|
},
|
|
transparent: true,
|
|
})
|
|
|
|
// Create mesh
|
|
this.mesh = new Mesh(this.gl, {
|
|
geometry: this.geometry,
|
|
program: this.program,
|
|
})
|
|
this.mesh.setParent(this.scene)
|
|
|
|
// Start globe angle with a random continent's position
|
|
if (this.options.rotationStart) {
|
|
this.mesh.rotation.y = degToRad(this.options.rotationStart * -1) || 0
|
|
}
|
|
|
|
// Add events
|
|
this.addEvents()
|
|
|
|
// Setup markers
|
|
if (this.markers) {
|
|
this.setupMarkers()
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Add events
|
|
*/
|
|
addEvents () {
|
|
// When clicking on globe
|
|
this.gl.canvas.addEventListener('mousedown', () => {
|
|
this.dragging = true
|
|
this.gl.canvas.classList.add('is-grabbing')
|
|
}, false)
|
|
|
|
// When releasing globe click
|
|
this.gl.canvas.addEventListener('mouseup', () => {
|
|
this.dragging = false
|
|
this.gl.canvas.classList.remove('is-grabbing')
|
|
}, false)
|
|
}
|
|
|
|
|
|
/**
|
|
* Markers
|
|
*/
|
|
// Get marker from DOM element
|
|
getMarker (id: string) {
|
|
const marker = this.parent.querySelector(`[data-location="${id}"]`)
|
|
if (marker) {
|
|
return marker
|
|
}
|
|
}
|
|
|
|
// Setup markers
|
|
setupMarkers () {
|
|
this.markers.forEach((marker: Marker) => {
|
|
const markerEl = this.getMarker(marker.slug)
|
|
|
|
// Entering marker
|
|
markerEl.addEventListener('mouseenter', () => {
|
|
this.hoveringMarker = true
|
|
}, false)
|
|
// Leaving marker
|
|
markerEl.addEventListener('mouseleave', () => {
|
|
this.hoveringMarker = false
|
|
}, false)
|
|
|
|
// Define position
|
|
const position = lonlatVec3(marker.lng, marker.lat)
|
|
|
|
// Scale marker position to fit globe size
|
|
marker.position = [position[0] *= 0.5, position[1] *= 0.5, position[2] *= 0.5]
|
|
|
|
// Position marker
|
|
const posX = (marker.position[0] + 1) * (this.width / 1.315)
|
|
const posY = (1 - marker.position[1]) * (this.height / 1.315)
|
|
markerEl.style.transform = `translate3d(${posX}px, ${posY}px, 0)`
|
|
|
|
console.log(marker)
|
|
return marker
|
|
})
|
|
}
|
|
|
|
// Update markers
|
|
updateMarkers () {
|
|
this.markers.forEach((marker: Marker) => {
|
|
// const markerEl = this.getMarker(marker.slug)
|
|
// const screenVector = new Vec3(0,0,0)
|
|
// screenVector.copy(marker.position)
|
|
// this.camera.project(screenVector)
|
|
|
|
|
|
// let posX = (screenVector.x + 1) * (this.options.width / 1.315)
|
|
// // // posX /= this.mesh.rotation.y
|
|
// let posY = (1 - screenVector.y) * (this.options.height / 1.315)
|
|
// markerEl.style.transform = `translate3d(${posX}px, ${posY}px, 0)`
|
|
})
|
|
}
|
|
|
|
|
|
/**
|
|
* Resize method
|
|
*/
|
|
resize () {
|
|
// this.renderer.setSize(window.innerWidth, window.innerHeight)
|
|
this.width = this.el.offsetWidth
|
|
this.height = this.el.offsetHeight
|
|
this.renderer.setSize(this.width, this.height)
|
|
this.camera.perspective({
|
|
aspect: this.gl.canvas.width / this.gl.canvas.height
|
|
})
|
|
}
|
|
|
|
|
|
/**
|
|
* Update method
|
|
*/
|
|
render () {
|
|
// Stop render if not dragging but hovering marker
|
|
if (!this.dragging && this.hoveringMarker) return
|
|
|
|
// Update globe rotation
|
|
if (this.params.autoRotate) {
|
|
this.mesh.rotation.y += this.params.speed
|
|
}
|
|
|
|
// Update controls and renderer
|
|
this.controls.update(this.params)
|
|
this.renderer.render({
|
|
scene: this.scene,
|
|
camera: this.camera,
|
|
})
|
|
|
|
// TODO: Update light
|
|
// this.light.set(this.camera.position)
|
|
// this.program.uniforms.u_lightWorldPosition.value = [this.mesh.rotation.y * 1, 50, 150]
|
|
|
|
// Update markers
|
|
this.updateMarkers()
|
|
}
|
|
|
|
|
|
/**
|
|
* Destroy
|
|
*/
|
|
destroy () {
|
|
console.log('destroy globe2')
|
|
|
|
this.gl = null
|
|
this.scene = null
|
|
this.camera = null
|
|
this.mesh = null
|
|
this.renderer = null
|
|
this.controls.remove()
|
|
|
|
if (this.pane) {
|
|
this.pane.dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Types
|
|
*/
|
|
type Options = {
|
|
el: HTMLElement
|
|
parent: HTMLElement
|
|
mapFile: string
|
|
dpr: number
|
|
autoRotate: boolean
|
|
speed: number
|
|
rotationStart?: number
|
|
markers?: any[]
|
|
pane?: boolean
|
|
}
|
|
export type Marker = {
|
|
name: string
|
|
slug: string
|
|
country: {
|
|
name: string
|
|
slug: string
|
|
flag: {
|
|
id: string
|
|
}
|
|
}
|
|
lat: number
|
|
lng: number
|
|
position?: number[]
|
|
}
|
|
|
|
|
|
/* ==========================================================================
|
|
HELPERS
|
|
========================================================================== */
|
|
/**
|
|
* Detect WebGL support
|
|
*/
|
|
function WebGLSupport () {
|
|
try {
|
|
var canvas = document.createElement('canvas')
|
|
return !!window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
|
|
} catch(e) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert lat/lng to Vec3
|
|
*/
|
|
function lonlatVec3 (longitude: number, latitude: number) {
|
|
const lat = latitude * Math.PI / 180
|
|
const lng = -longitude * Math.PI / 180
|
|
return new Vec3(
|
|
Math.cos(lat) * Math.cos(lng),
|
|
Math.sin(lat),
|
|
Math.cos(lat) * Math.sin(lng)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Convert Degrees to Radians
|
|
*/
|
|
const degToRad = (deg: number) => deg * Math.PI / 180 |