-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🐹👩🏻🦽 ↝ Merge pull request #103 from Signal-K/GP-16-New-globe-compone…
…nt-interactive [GP-16 SGV2-10] 🍒🐹 ↝ Gp 16 new globe component interactive
- Loading branch information
Showing
13 changed files
with
1,471 additions
and
258 deletions.
There are no files selected for viewing
70 changes: 70 additions & 0 deletions
70
components/Content/Planets/Construction/GlobeClickable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { useEffect, useRef } from "react"; | ||
import * as THREE from "three"; | ||
import { Sprite, SpriteMaterial, TextureLoader } from "three"; | ||
import { useFrame } from "@react-three/fiber"; | ||
|
||
interface Landmass { | ||
lat: number; | ||
lng: number; | ||
imageURL: string; | ||
onClickFunction: () => void; | ||
} | ||
|
||
interface ClickableImagesProps { | ||
landmasses: Landmass[]; | ||
} | ||
|
||
function ClickableImages({ landmasses }: ClickableImagesProps) { | ||
const groupRef = useRef<THREE.Group | null>(null); | ||
|
||
useEffect(() => { | ||
if (groupRef.current) { | ||
landmasses.forEach((landmass) => { | ||
const { lat, lng, imageURL, onClickFunction } = landmass; | ||
|
||
// Load texture | ||
const textureLoader = new TextureLoader(); | ||
const texture = textureLoader.load(imageURL); | ||
|
||
// Create a sprite with the texture | ||
const spriteMaterial = new SpriteMaterial({ map: texture }); | ||
const sprite = new Sprite(spriteMaterial); | ||
|
||
// Set the size of the sprite | ||
const size = 8; // Adjust size as needed (e.g. 8 for Tailwind scale) | ||
sprite.scale.set(size, size, 1); | ||
|
||
// Convert lat/lng to 3D coordinates | ||
const radius = 100; // Adjust radius as needed | ||
const phi = (90 - lat) * (Math.PI / 180); | ||
const theta = lng * (Math.PI / 180); | ||
sprite.position.set( | ||
radius * Math.sin(phi) * Math.cos(theta), | ||
radius * Math.cos(phi), | ||
radius * Math.sin(phi) * Math.sin(theta) | ||
); | ||
|
||
// Create a click handler for the sprite | ||
const handleClick = (event: any) => { | ||
event.stopPropagation(); | ||
onClickFunction(); | ||
}; | ||
|
||
// Add event listener to the sprite for the 'click' event | ||
sprite.addEventListener('click', handleClick); | ||
|
||
// Add the sprite to the group | ||
groupRef.current.add(sprite); | ||
}); | ||
} | ||
}, [landmasses]); | ||
|
||
// Use useFrame hook to continuously update the scene if needed | ||
useFrame(() => { | ||
// Add any animation logic here if required | ||
}); | ||
|
||
return <group ref={groupRef} />; | ||
} | ||
|
||
export default ClickableImages; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
import { useEffect, useRef, useState } from "react"; | ||
import { Color, Scene, Fog, PerspectiveCamera, Vector3 } from "three"; | ||
import ThreeGlobe from "three-globe"; | ||
import { useThree, Object3DNode, Canvas, extend } from "@react-three/fiber"; | ||
import { OrbitControls } from "@react-three/drei"; | ||
import countries from "../../../context/data/globe.json"; | ||
declare module "@react-three/fiber" { | ||
interface ThreeElements { | ||
threeGlobe: Object3DNode<ThreeGlobe, typeof ThreeGlobe>; | ||
} | ||
} | ||
|
||
extend({ ThreeGlobe }); | ||
|
||
const RING_PROPAGATION_SPEED = 3; | ||
const aspect = 1.2; | ||
const cameraZ = 300; | ||
|
||
type Position = { | ||
order: number; | ||
startLat: number; | ||
startLng: number; | ||
endLat: number; | ||
endLng: number; | ||
arcAlt: number; | ||
color: string; | ||
}; | ||
|
||
export type GlobeConfig = { | ||
pointSize?: number; | ||
globeColor?: string; | ||
showAtmosphere?: boolean; | ||
atmosphereColor?: string; | ||
atmosphereAltitude?: number; | ||
emissive?: string; | ||
emissiveIntensity?: number; | ||
shininess?: number; | ||
polygonColor?: string; | ||
ambientLight?: string; | ||
directionalLeftLight?: string; | ||
directionalTopLight?: string; | ||
pointLight?: string; | ||
arcTime?: number; | ||
arcLength?: number; | ||
rings?: number; | ||
maxRings?: number; | ||
initialPosition?: { | ||
lat: number; | ||
lng: number; | ||
}; | ||
autoRotate?: boolean; | ||
autoRotateSpeed?: number; | ||
}; | ||
|
||
interface WorldProps { | ||
globeConfig: GlobeConfig; | ||
data: Position[]; | ||
} | ||
|
||
let numbersOfRings = [0]; | ||
|
||
export function Globe({ globeConfig, data }: WorldProps) { | ||
const [globeData, setGlobeData] = useState< | ||
| { | ||
size: number; | ||
order: number; | ||
color: (t: number) => string; | ||
lat: number; | ||
lng: number; | ||
}[] | ||
| null | ||
>(null); | ||
|
||
const globeRef = useRef<ThreeGlobe | null>(null); | ||
|
||
const defaultProps = { | ||
pointSize: 1, | ||
atmosphereColor: "#ffffff", | ||
showAtmosphere: true, | ||
atmosphereAltitude: 0.1, | ||
polygonColor: "rgba(255,255,255,0.7)", | ||
globeColor: "#1d072e", | ||
emissive: "#000000", | ||
emissiveIntensity: 0.1, | ||
shininess: 0.9, | ||
arcTime: 2000, | ||
arcLength: 0.9, | ||
rings: 1, | ||
maxRings: 3, | ||
...globeConfig, | ||
}; | ||
|
||
useEffect(() => { | ||
if (globeRef.current) { | ||
_buildData(); | ||
_buildMaterial(); | ||
} | ||
}, [globeRef.current]); | ||
|
||
const _buildMaterial = () => { | ||
if (!globeRef.current) return; | ||
|
||
const globeMaterial = globeRef.current.globeMaterial() as unknown as { | ||
color: Color; | ||
emissive: Color; | ||
emissiveIntensity: number; | ||
shininess: number; | ||
}; | ||
globeMaterial.color = new Color(globeConfig.globeColor); | ||
globeMaterial.emissive = new Color(globeConfig.emissive); | ||
globeMaterial.emissiveIntensity = globeConfig.emissiveIntensity || 0.1; | ||
globeMaterial.shininess = globeConfig.shininess || 0.9; | ||
}; | ||
|
||
const _buildData = () => { | ||
const arcs = data; | ||
let points = []; | ||
for (let i = 0; i < arcs.length; i++) { | ||
const arc = arcs[i]; | ||
const rgb = hexToRgb(arc.color) as { r: number; g: number; b: number }; | ||
points.push({ | ||
size: defaultProps.pointSize, | ||
order: arc.order, | ||
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`, | ||
lat: arc.startLat, | ||
lng: arc.startLng, | ||
}); | ||
points.push({ | ||
size: defaultProps.pointSize, | ||
order: arc.order, | ||
color: (t: number) => `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${1 - t})`, | ||
lat: arc.endLat, | ||
lng: arc.endLng, | ||
}); | ||
} | ||
|
||
// remove duplicates for same lat and lng | ||
const filteredPoints = points.filter( | ||
(v, i, a) => | ||
a.findIndex((v2) => | ||
["lat", "lng"].every( | ||
(k) => v2[k as "lat" | "lng"] === v[k as "lat" | "lng"] | ||
) | ||
) === i | ||
); | ||
|
||
setGlobeData(filteredPoints); | ||
}; | ||
|
||
useEffect(() => { | ||
if (globeRef.current && globeData) { | ||
globeRef.current | ||
.hexPolygonsData(countries.features) | ||
.hexPolygonResolution(3) | ||
.hexPolygonMargin(0.7) | ||
.showAtmosphere(defaultProps.showAtmosphere) | ||
.atmosphereColor(defaultProps.atmosphereColor) | ||
.atmosphereAltitude(defaultProps.atmosphereAltitude) | ||
.hexPolygonColor((e) => { | ||
return defaultProps.polygonColor; | ||
}); | ||
startAnimation(); | ||
} | ||
}, [globeData]); | ||
|
||
const startAnimation = () => { | ||
if (!globeRef.current || !globeData) return; | ||
|
||
globeRef.current | ||
.arcsData(data) | ||
.arcStartLat((d) => (d as { startLat: number }).startLat * 1) | ||
.arcStartLng((d) => (d as { startLng: number }).startLng * 1) | ||
.arcEndLat((d) => (d as { endLat: number }).endLat * 1) | ||
.arcEndLng((d) => (d as { endLng: number }).endLng * 1) | ||
.arcColor((e: any) => (e as { color: string }).color) | ||
.arcAltitude((e) => { | ||
return (e as { arcAlt: number }).arcAlt * 1; | ||
}) | ||
.arcStroke((e) => { | ||
return [0.32, 0.28, 0.3][Math.round(Math.random() * 2)]; | ||
}) | ||
.arcDashLength(defaultProps.arcLength) | ||
.arcDashInitialGap((e) => (e as { order: number }).order * 1) | ||
.arcDashGap(15) | ||
.arcDashAnimateTime((e) => defaultProps.arcTime); | ||
|
||
globeRef.current | ||
.pointsData(data) | ||
.pointColor((e) => (e as { color: string }).color) | ||
.pointsMerge(true) | ||
.pointAltitude(0.0) | ||
.pointRadius(2); | ||
|
||
globeRef.current | ||
.ringsData([]) | ||
.ringColor((e: any) => (t: any) => e.color(t)) | ||
.ringMaxRadius(defaultProps.maxRings) | ||
.ringPropagationSpeed(RING_PROPAGATION_SPEED) | ||
.ringRepeatPeriod( | ||
(defaultProps.arcTime * defaultProps.arcLength) / defaultProps.rings | ||
); | ||
}; | ||
|
||
useEffect(() => { | ||
if (!globeRef.current || !globeData) return; | ||
|
||
const interval = setInterval(() => { | ||
if (!globeRef.current || !globeData) return; | ||
numbersOfRings = genRandomNumbers( | ||
0, | ||
data.length, | ||
Math.floor((data.length * 4) / 5) | ||
); | ||
|
||
globeRef.current.ringsData( | ||
globeData.filter((d, i) => numbersOfRings.includes(i)) | ||
); | ||
}, 2000); | ||
|
||
return () => { | ||
clearInterval(interval); | ||
}; | ||
}, [globeRef.current, globeData]); | ||
|
||
return ( | ||
<> | ||
<threeGlobe ref={globeRef} /> | ||
</> | ||
); | ||
} | ||
|
||
export function WebGLRendererConfig() { | ||
const { gl, size } = useThree(); | ||
|
||
useEffect(() => { | ||
gl.setPixelRatio(window.devicePixelRatio); | ||
gl.setSize(size.width, size.height); | ||
gl.setClearColor(0xffaaff, 0); | ||
}, []); | ||
|
||
return null; | ||
} | ||
|
||
export function World(props: WorldProps) { | ||
const { globeConfig } = props; | ||
const scene = new Scene(); | ||
scene.fog = new Fog(0xffffff, 400, 2000); | ||
return ( | ||
<Canvas scene={scene} camera={new PerspectiveCamera(50, aspect, 180, 1800)}> | ||
<WebGLRendererConfig /> | ||
<ambientLight color={globeConfig.ambientLight} intensity={0.6} /> | ||
<directionalLight | ||
color={globeConfig.directionalLeftLight} | ||
position={new Vector3(-400, 100, 400)} | ||
/> | ||
<directionalLight | ||
color={globeConfig.directionalTopLight} | ||
position={new Vector3(-200, 500, 200)} | ||
/> | ||
<pointLight | ||
color={globeConfig.pointLight} | ||
position={new Vector3(-200, 500, 200)} | ||
intensity={0.8} | ||
/> | ||
<Globe {...props} /> | ||
<OrbitControls | ||
enablePan={false} | ||
enableZoom={false} | ||
minDistance={cameraZ} | ||
maxDistance={cameraZ} | ||
autoRotateSpeed={1} | ||
autoRotate={true} | ||
minPolarAngle={Math.PI / 3.5} | ||
maxPolarAngle={Math.PI - Math.PI / 3} | ||
/> | ||
</Canvas> | ||
); | ||
} | ||
|
||
export function hexToRgb(hex: string) { | ||
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; | ||
hex = hex.replace(shorthandRegex, function (m, r, g, b) { | ||
return r + r + g + g + b + b; | ||
}); | ||
|
||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); | ||
return result | ||
? { | ||
r: parseInt(result[1], 16), | ||
g: parseInt(result[2], 16), | ||
b: parseInt(result[3], 16), | ||
} | ||
: null; | ||
} | ||
|
||
export function genRandomNumbers(min: number, max: number, count: number) { | ||
const arr = []; | ||
while (arr.length < count) { | ||
const r = Math.floor(Math.random() * (max - min)) + min; | ||
if (arr.indexOf(r) === -1) arr.push(r); | ||
} | ||
|
||
return arr; | ||
} |
Oops, something went wrong.