Files
housesof/apps/website/src/modules/globe/index.ts
Félix Péault 7efc1842bd feat: set correct sun angle from current time
Thanks ChatGPT for that one!!

How to find the current sun angle from Earth's rotation in javascript?
A hint: Where is the sun shining on the earth at the time I visit the site? I guess the time zone that's closest to noon.

the dayTime variable needs to be from 0 to 1

const d = degToRad(360 / dayTime)
const sunPos = new Vec3(
    Math.cos(d),
    Math.sin(d) * Math.sin(0),
    Math.sin(d) * Math.cos(0)
)
2023-02-10 15:35:36 +01:00

398 lines
11 KiB
TypeScript

// @ts-nocheck
import { Renderer, Camera, Vec3, Orbit, Sphere, Transform, Program, Mesh, Texture } from 'ogl'
// Shaders
import VERTEX_SHADER from '$modules/globe/vertex.glsl?raw'
import FRAGMENT_SHADER from '$modules/globe/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 || []
this.zoom = 1.3075
// Calculate local time for sun position
const date = new Date()
const localHour = date.getHours() + date.getTimezoneOffset() / 60
this.options.sunAngle = (localHour - 12) / 12
// Parameters
this.params = {
autoRotate: options.autoRotate,
speed: options.speed,
enableMarkers: options.enableMarkers,
enableMarkersLinks: options.enableMarkersLinks,
sunAngle: options.sunAngle || 0,
}
// Misc
this.isDev = import.meta.env.DEV
this.hoveringMarker = false
this.hoveringMarkerTimeout = 0
this.lastFrame = now()
this.dragging = false
this.webgl = WebGLSupport() !== null
this.pane = undefined
// Run globe after check for WebGL support
if (this.webgl) {
this.build()
this.resize()
}
// 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)
this.camera.position.set(setFromSphericalCoords(
this.zoom,
degToRad(this.options.rotationStart.y || 40), // phi: y
degToRad(this.options.rotationStart.x || 0), // theta: x
))
this.camera.lookAt(0,0,0)
// Create controls
this.controls = new Orbit(this.camera, {
element: this.el,
enableZoom: false,
enablePan: false,
ease: 0.2,
minPolarAngle: Math.PI / 4,
maxPolarAngle: Math.PI / 1.85,
})
// 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: 75,
heightSegments: 75,
})
// Add map texture
const mapWorld = new Texture(this.gl)
const img = new Image()
img.onload = () => (mapWorld.image = img)
img.src = this.options.mapFile
// Dark map texture
const mapDark = new Texture(this.gl)
const imgDark = new Image()
imgDark.onload = () => (mapDark.image = imgDark)
imgDark.src = this.options.mapFileDark
// Create light
const sunPosition = new Vec3(
Math.cos(this.params.sunAngle),
Math.sin(this.params.sunAngle) * Math.sin(0),
Math.sin(this.params.sunAngle) * Math.cos(0)
)
// Create program
const program = new Program(this.gl, {
vertex: VERTEX_SHADER,
fragment: FRAGMENT_SHADER,
uniforms: {
u_dt: { value: 0 },
map: { value: mapWorld }, // Map Texture
mapDark: { value: mapDark }, // Map Dark Texture
sunPosition: { value: sunPosition },
},
cullFace: null,
})
// Create globe mesh
this.globe = new Mesh(this.gl, {
geometry: this.geometry,
program,
})
this.globe.setParent(this.scene)
// 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)
// Update marker position
this.updateMarkerPosition(marker, markerEl)
// Entering marker
markerEl.addEventListener('mouseenter', () => {
this.hoveringMarker = true
clearTimeout(this.hoveringMarkerTimeout)
}, false)
// Leaving marker
markerEl.addEventListener('mouseleave', () => {
this.hoveringMarkerTimeout = setTimeout(() => {
this.hoveringMarker = false
}, 300)
}, false)
return marker
})
}
// Update marker position
updateMarkerPosition (marker: Marker, markerEl: HTMLElement) {
// Get vec3 position from lat/long
const position = latLonToVec3(marker.lat, marker.lng)
const screenVector = new Vec3(position.x, position.y, position.z)
// Apply transformation to marker from globe world matrix
screenVector.applyMatrix4(this.globe.worldMatrix)
// Then project marker on camera
this.camera.project(screenVector)
// Position marker
const posX = ((screenVector[0] + 1) / 2) * this.width
const posY = (1. - (screenVector[1] + 1) / 2) * this.height
markerEl.style.transform = `translate3d(${posX}px, ${posY}px, 0)`
// Hide marker if behind globe
markerEl.classList.toggle('is-hidden', screenVector[2] > 0.82)
}
// Update markers
updateMarkers () {
this.markers.forEach((marker: Marker) => {
const markerEl = this.getMarker(marker.slug)
// Update marker position
this.updateMarkerPosition(marker, markerEl)
})
}
// Enable or disable markers
enableMarkers (state: boolean) {
this.markers.forEach((marker: Marker) => {
const markerEl = this.getMarker(marker.slug)
markerEl.classList.toggle('is-disabled', !state)
})
}
// Hide markers
hideMarkers () {
this.markers.forEach((marker: Marker) => {
const markerEl = this.getMarker(marker.slug)
markerEl.classList.add('is-hidden')
})
}
/**
* Resize method
*/
resize () {
if (this.renderer) {
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 () {
const delta = (now() - this.lastFrame) / 1000
this.lastFrame = now()
// Rotate globe if not dragging neither hovering marker
if (this.params.autoRotate && !this.hoveringMarker) {
this.globe.rotation.y += this.params.speed * delta
}
// Update controls and renderer
this.controls.update()
this.renderer.render({
scene: this.scene,
camera: this.camera,
})
// Update markers
if (this.params.enableMarkers) {
this.updateMarkers()
// Enable or disable interactivity
this.enableMarkers(this.params.enableMarkersLinks)
} else {
this.hideMarkers()
}
}
/**
* Destroy
*/
destroy () {
this.gl = null
this.scene = null
this.camera = null
this.globe = null
this.renderer = null
this.controls.remove()
if (this.pane) {
this.pane.dispose()
}
if (this.isDev) {
console.log('globe: destroy')
}
}
}
/**
* Types
*/
type Options = {
el: HTMLElement
parent: HTMLElement
mapFile: string
mapFileDark: string
dpr: number
autoRotate: boolean
speed: number
sunAngle: number
rotationStart?: { x: number, y: number }
enableMarkers?: boolean
enableMarkersLinks?: boolean
markers?: any[]
pane?: boolean
}
export type Marker = {
name: string
slug: string
country: {
name: string
slug: string
flag: {
id: string
}
}
lat: number
lng: number
}
/* ==========================================================================
HELPERS
========================================================================== */
/**
* Detect WebGL support
*/
const WebGLSupport = (): boolean => {
try {
const canvas = document.createElement('canvas')
return !!window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
} catch (e) {
return false
}
}
/**
* Convert lat/lng to Vec3
*/
const latLonToVec3 = (lat: number, lng: number) => {
const phi = (90 - lat) * (Math.PI / 180)
const theta = (lng + 180) * (Math.PI / 180)
const x = -((0.5) * Math.sin(phi) * Math.cos(theta))
const z = ((0.5) * Math.sin(phi) * Math.sin(theta))
const y = ((0.5) * Math.cos(phi))
return new Vec3(x,y,z)
}
/**
* Get position from spherical coordinates
*/
const setFromSphericalCoords = (radius: number, phi: number, theta: number) => {
const sinPhiRadius = Math.sin(phi) * radius
const x = sinPhiRadius * Math.sin(theta)
const y = Math.cos(phi) * radius
const z = sinPhiRadius * Math.cos(theta)
return new Vec3(x,y,z)
}
/**
* Convert Degrees to Radians
*/
const degToRad = (deg: number) => deg * Math.PI / 180
/**
* Get current timestamp (performance or Date)
*/
const now = () => (typeof performance === 'undefined' ? Date : performance).now()