first commit
This commit is contained in:
parent
a3c0b0da5a
commit
46a7d921d2
68
index.html
Normal file
68
index.html
Normal file
@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Planet Visualization</title>
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
|
||||
<!-- External Libraries -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Screen 1: Settings Screen -->
|
||||
<div id="settings-screen" class="screen active">
|
||||
<div class="screen-content">
|
||||
<h1>Planet Generator</h1>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="points-count">Number of Points:</label>
|
||||
<input type="number" id="points-count" value="5000" min="1000" max="50000" step="1000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="color-scheme">Color Scheme:</label>
|
||||
<select id="color-scheme">
|
||||
<option value="terrain">Terrain</option>
|
||||
<option value="random">Random Colors</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="generate-btn" class="primary-btn">Generate Planet</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Screen 2: Loading Screen -->
|
||||
<div id="loading-screen" class="screen">
|
||||
<div class="screen-content">
|
||||
<h2>Generating Planet...</h2>
|
||||
<div class="progress-container">
|
||||
<div id="progress-bar" class="progress-bar"></div>
|
||||
</div>
|
||||
<div id="progress-text">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Screen 3: Visualization Screen -->
|
||||
<div id="visualization-screen" class="screen">
|
||||
<div id="vis-controls">
|
||||
<div>
|
||||
<button id="back-btn">Back to Settings</button>
|
||||
<button id="regenerate-btn">Regenerate Planet</button>
|
||||
<label for="auto-rotate">Rotate: </label>
|
||||
<input type="checkbox" id="auto-rotate" checked>
|
||||
</div>
|
||||
</div>
|
||||
<div id="visualization">
|
||||
<div id="threejs-container"></div>
|
||||
<div id="d3-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script src="planet_visualization.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
536
planet_visualization.js
Normal file
536
planet_visualization.js
Normal file
@ -0,0 +1,536 @@
|
||||
// Global variables
|
||||
let planetData = null;
|
||||
let threejsScene, threejsCamera, threejsRenderer, threejsControls, threejsSphere;
|
||||
let d3Svg, d3Projection, d3Path;
|
||||
let progressInterval;
|
||||
|
||||
// Screen management
|
||||
function showScreen(screenId) {
|
||||
// Hide all screens
|
||||
document.querySelectorAll('.screen').forEach(screen => {
|
||||
screen.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show the requested screen
|
||||
document.getElementById(screenId).classList.add('active');
|
||||
|
||||
// Initialize visualization if showing that screen
|
||||
if (screenId === 'visualization-screen' && planetData) {
|
||||
// Need a small delay for the containers to be visible with correct dimensions
|
||||
setTimeout(() => {
|
||||
if (!threejsScene) {
|
||||
initVisualizations();
|
||||
} else {
|
||||
// If already initialized, handle resize for containers that were hidden
|
||||
handleResize();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
function handleResize() {
|
||||
if (threejsRenderer && threejsCamera) {
|
||||
const container = document.getElementById('threejs-container');
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
threejsCamera.aspect = width / height;
|
||||
threejsCamera.updateProjectionMatrix();
|
||||
threejsRenderer.setSize(width, height);
|
||||
}
|
||||
|
||||
if (d3Svg && d3Projection) {
|
||||
const container = document.getElementById('d3-container');
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
d3Svg.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
d3Projection.scale(width / (2 * Math.PI) * 0.9)
|
||||
.translate([width / 2, height / 1.8]);
|
||||
|
||||
updateVisualization();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the 3D visualization with Three.js
|
||||
function init3DVisualization() {
|
||||
const container = document.getElementById('threejs-container');
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
// Create scene
|
||||
threejsScene = new THREE.Scene();
|
||||
threejsScene.background = new THREE.Color(0x000000);
|
||||
|
||||
// Create camera
|
||||
threejsCamera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||
threejsCamera.position.z = 2;
|
||||
|
||||
// Add polar axis helper
|
||||
const axisHelper = new THREE.Group();
|
||||
|
||||
// North-South pole axis (red)
|
||||
const poleGeometry = new THREE.CylinderGeometry(0.01, 0.01, 2.4, 8);
|
||||
const poleMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
|
||||
const poleAxis = new THREE.Mesh(poleGeometry, poleMaterial);
|
||||
// Correctly align with y-axis (poles should be on y-axis)
|
||||
poleAxis.rotation.z = 0;
|
||||
axisHelper.add(poleAxis);
|
||||
|
||||
// North pole cap (red)
|
||||
const northCapGeometry = new THREE.ConeGeometry(0.04, 0.1, 8);
|
||||
const northCap = new THREE.Mesh(northCapGeometry, poleMaterial);
|
||||
northCap.position.set(0, 1.25, 0);
|
||||
northCap.rotation.x = Math.PI; // Point up
|
||||
axisHelper.add(northCap);
|
||||
|
||||
// South pole cap (red)
|
||||
const southCapGeometry = new THREE.ConeGeometry(0.04, 0.1, 8);
|
||||
const southCap = new THREE.Mesh(southCapGeometry, poleMaterial);
|
||||
southCap.position.set(0, -1.25, 0);
|
||||
axisHelper.add(southCap);
|
||||
|
||||
threejsScene.add(axisHelper);
|
||||
|
||||
// Create renderer
|
||||
threejsRenderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
threejsRenderer.setSize(width, height);
|
||||
container.appendChild(threejsRenderer.domElement);
|
||||
|
||||
// Add orbit controls
|
||||
threejsControls = new THREE.OrbitControls(threejsCamera, threejsRenderer.domElement);
|
||||
threejsControls.enableDamping = true;
|
||||
threejsControls.dampingFactor = 0.05;
|
||||
|
||||
// Add ambient light
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
threejsScene.add(ambientLight);
|
||||
|
||||
// Add directional light
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
directionalLight.position.set(1, 1, 1);
|
||||
threejsScene.add(directionalLight);
|
||||
|
||||
// Start animation loop
|
||||
animate();
|
||||
}
|
||||
|
||||
// Initialize the 2D visualization with D3.js
|
||||
function init2DVisualization() {
|
||||
const container = document.getElementById('d3-container');
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
// Create SVG
|
||||
d3Svg = d3.select('#d3-container')
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
// Add a background rectangle representing the ocean
|
||||
d3Svg.append('rect')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('fill', '#a4d1e9'); // Ocean blue
|
||||
|
||||
// Create projection (Mercator) similar to standard world maps
|
||||
d3Projection = d3.geoMercator()
|
||||
.scale(width / (2 * Math.PI) * 0.9)
|
||||
.translate([width / 2, height / 1.8]);
|
||||
|
||||
// Create path generator
|
||||
d3Path = d3.geoPath()
|
||||
.projection(d3Projection);
|
||||
|
||||
// Draw graticule (grid lines)
|
||||
const graticule = d3.geoGraticule()
|
||||
.step([15, 15]); // Grid every 15 degrees
|
||||
|
||||
d3Svg.append('path')
|
||||
.datum(graticule)
|
||||
.attr('class', 'graticule')
|
||||
.attr('d', d3Path)
|
||||
.style('fill', 'none')
|
||||
.style('stroke', '#ffffff')
|
||||
.style('stroke-width', '0.5px')
|
||||
.style('opacity', 0.5);
|
||||
|
||||
// Draw equator
|
||||
const equator = {
|
||||
type: "LineString",
|
||||
coordinates: [[-180, 0], [-90, 0], [0, 0], [90, 0], [180, 0]]
|
||||
};
|
||||
|
||||
d3Svg.append('path')
|
||||
.datum(equator)
|
||||
.attr('class', 'equator')
|
||||
.attr('d', d3Path)
|
||||
.style('fill', 'none')
|
||||
.style('stroke', '#00ffff')
|
||||
.style('stroke-width', '2px');
|
||||
|
||||
// Draw prime meridian (0° longitude)
|
||||
// const primeMeridian = {
|
||||
// type: "LineString",
|
||||
// coordinates: [[0, -90], [0, -45], [0, 0], [0, 45], [0, 90]]
|
||||
// };
|
||||
|
||||
// d3Svg.append('path')
|
||||
// .datum(primeMeridian)
|
||||
// .attr('class', 'prime-meridian')
|
||||
// .attr('d', d3Path)
|
||||
// .style('fill', 'none')
|
||||
// .style('stroke', '#ff0000')
|
||||
// .style('stroke-width', '2px');
|
||||
|
||||
// Draw continental outlines (simplified)
|
||||
// This just adds some landmass-like shapes to make it feel more Earth-like
|
||||
const mockContinents = [
|
||||
{ // North America (simplified)
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[-140, 70], [-120, 60], [-100, 50], [-80, 30],
|
||||
[-90, 20], [-100, 15], [-120, 30], [-130, 50], [-140, 70]
|
||||
]]
|
||||
},
|
||||
{ // South America (simplified)
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[-80, 10], [-60, 0], [-50, -10], [-60, -30],
|
||||
[-70, -50], [-80, -30], [-90, -10], [-80, 10]
|
||||
]]
|
||||
},
|
||||
{ // Europe/Africa (very simplified)
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[0, 60], [20, 40], [30, 30], [20, 10], [10, 0],
|
||||
[20, -10], [30, -30], [20, -40], [0, -30], [-10, 0], [0, 60]
|
||||
]]
|
||||
},
|
||||
{ // Asia (simplified)
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[30, 60], [60, 70], [100, 60], [130, 40], [110, 20],
|
||||
[100, 0], [80, 10], [60, 30], [40, 40], [30, 60]
|
||||
]]
|
||||
},
|
||||
{ // Australia (simplified)
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[110, -20], [130, -25], [140, -35], [130, -40],
|
||||
[120, -35], [110, -30], [110, -20]
|
||||
]]
|
||||
}
|
||||
];
|
||||
|
||||
// Add continent outlines with very low opacity to provide visual reference
|
||||
mockContinents.forEach(continent => {
|
||||
d3Svg.append('path')
|
||||
.datum(continent)
|
||||
.attr('class', 'continent-outline')
|
||||
.attr('d', d3Path);
|
||||
});
|
||||
|
||||
// Add labels for orientation
|
||||
const labels = [
|
||||
{ text: "North Pole", coords: [0, 85], anchor: "middle" },
|
||||
{ text: "South Pole", coords: [0, -85], anchor: "middle" },
|
||||
{ text: "Equator", coords: [-170, 0], anchor: "start" },
|
||||
//{ text: "Prime Meridian", coords: [5, 45], anchor: "start" }
|
||||
];
|
||||
|
||||
labels.forEach(label => {
|
||||
const [x, y] = d3Projection(label.coords);
|
||||
d3Svg.append('text')
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('class', 'map-label')
|
||||
.attr('text-anchor', label.anchor)
|
||||
.text(label.text);
|
||||
});
|
||||
}
|
||||
|
||||
// Create a Three.js sphere from points
|
||||
function createThreeJSSphere(points) {
|
||||
// Remove existing sphere if any
|
||||
if (threejsSphere) {
|
||||
threejsScene.remove(threejsSphere);
|
||||
}
|
||||
|
||||
// Create a group to hold the sphere and equator
|
||||
threejsSphere = new THREE.Group();
|
||||
|
||||
// Create geometry
|
||||
const geometry = new THREE.SphereGeometry(1, 32, 32);
|
||||
|
||||
// Create material
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
vertexColors: true,
|
||||
flatShading: true
|
||||
});
|
||||
|
||||
// Add equator
|
||||
const equatorGeometry = new THREE.TorusGeometry(1.01, 0.005, 16, 100);
|
||||
const equatorMaterial = new THREE.MeshBasicMaterial({ color: 0x00ffff });
|
||||
const equator = new THREE.Mesh(equatorGeometry, equatorMaterial);
|
||||
equator.rotation.x = Math.PI / 2; // Rotate to lie on the x-z plane
|
||||
threejsSphere.add(equator);
|
||||
|
||||
// Add colors to geometry
|
||||
const colors = [];
|
||||
const positionAttribute = geometry.getAttribute('position');
|
||||
|
||||
// For each vertex in the sphere
|
||||
for (let i = 0; i < positionAttribute.count; i++) {
|
||||
const vertex = new THREE.Vector3();
|
||||
vertex.fromBufferAttribute(positionAttribute, i);
|
||||
vertex.normalize();
|
||||
|
||||
// Find closest point in our dataset
|
||||
let minDist = Infinity;
|
||||
let closestPoint = null;
|
||||
|
||||
for (const point of points) {
|
||||
const pointVec = new THREE.Vector3(point.x, point.y, point.z);
|
||||
const dist = vertex.distanceTo(pointVec);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closestPoint = point;
|
||||
}
|
||||
}
|
||||
|
||||
// Set color from closest point
|
||||
colors.push(
|
||||
closestPoint.color[0],
|
||||
closestPoint.color[1],
|
||||
closestPoint.color[2]
|
||||
);
|
||||
}
|
||||
|
||||
// Add colors to geometry
|
||||
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
||||
|
||||
// Create mesh for the sphere
|
||||
const sphereMesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
// Add sphere to the group
|
||||
threejsSphere.add(sphereMesh);
|
||||
|
||||
// Add group to scene
|
||||
threejsScene.add(threejsSphere);
|
||||
}
|
||||
|
||||
// Create a D3.js map from points
|
||||
function createD3Map(points) {
|
||||
// Clear existing points but preserve graticule, equator, and axis
|
||||
d3Svg.selectAll('.planet-point').remove();
|
||||
|
||||
// Create groups for continents to improve organization
|
||||
const pointsGroup = d3Svg.append('g').attr('class', 'points-group');
|
||||
|
||||
// Plot each point on the map
|
||||
points.forEach(point => {
|
||||
// Convert spherical coordinates to longitude/latitude
|
||||
// phi goes from -π to π, convert to -180 to 180 degrees (longitude)
|
||||
const lon = (point.phi * 180 / Math.PI);
|
||||
|
||||
// theta goes from 0 to π, convert to 90 to -90 degrees (latitude)
|
||||
const lat = 90 - (point.theta * 180 / Math.PI);
|
||||
|
||||
// Skip points that might cause projection issues at the poles
|
||||
if (lat > 85 || lat < -85) return;
|
||||
|
||||
// Only draw the point if it projects properly
|
||||
const projected = d3Projection([lon, lat]);
|
||||
if (projected) {
|
||||
pointsGroup.append('circle')
|
||||
.attr('class', 'planet-point')
|
||||
.attr('cx', projected[0])
|
||||
.attr('cy', projected[1])
|
||||
.attr('r', 1.5)
|
||||
.style('fill', `rgb(${point.color[0] * 255}, ${point.color[1] * 255}, ${point.color[2] * 255})`)
|
||||
.style('opacity', 0.8);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Animation loop for Three.js
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
// Update controls
|
||||
if (threejsControls) {
|
||||
threejsControls.update();
|
||||
}
|
||||
|
||||
// Auto-rotate if enabled and on visualization screen
|
||||
if (document.getElementById('visualization-screen').classList.contains('active') &&
|
||||
document.getElementById('auto-rotate').checked &&
|
||||
threejsSphere) {
|
||||
threejsSphere.rotation.y += 0.005;
|
||||
}
|
||||
|
||||
// Render scene
|
||||
if (threejsRenderer && threejsScene && threejsCamera) {
|
||||
threejsRenderer.render(threejsScene, threejsCamera);
|
||||
}
|
||||
}
|
||||
|
||||
// Update visualizations with new data
|
||||
function updateVisualization() {
|
||||
if (planetData && planetData.points) {
|
||||
createThreeJSSphere(planetData.points);
|
||||
createD3Map(planetData.points);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize visualizations
|
||||
function initVisualizations() {
|
||||
// Only initialize if not already done
|
||||
if (!threejsScene) {
|
||||
init3DVisualization();
|
||||
}
|
||||
|
||||
if (!d3Svg) {
|
||||
init2DVisualization();
|
||||
}
|
||||
|
||||
updateVisualization();
|
||||
}
|
||||
|
||||
// Start progress bar simulation
|
||||
function startProgressBar() {
|
||||
let progress = 0;
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
|
||||
clearInterval(progressInterval);
|
||||
|
||||
progressBar.style.width = '0%';
|
||||
progressText.textContent = '0%';
|
||||
|
||||
// Simulate progress updates
|
||||
progressInterval = setInterval(() => {
|
||||
if (progress >= 100) {
|
||||
clearInterval(progressInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make progress increases less predictable
|
||||
const increment = Math.random() * 5 + 1;
|
||||
progress = Math.min(progress + increment, 100);
|
||||
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressText.textContent = `${Math.round(progress)}%`;
|
||||
|
||||
// When complete, show visualization
|
||||
if (progress >= 100) {
|
||||
setTimeout(() => {
|
||||
showScreen('visualization-screen');
|
||||
}, 500); // Short delay for animation
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Generate planet
|
||||
function generatePlanet() {
|
||||
const pointsCount = parseInt(document.getElementById('points-count').value);
|
||||
const colorScheme = document.getElementById('color-scheme').value;
|
||||
|
||||
// Show loading screen
|
||||
showScreen('loading-screen');
|
||||
|
||||
// Start progress bar
|
||||
startProgressBar();
|
||||
|
||||
// Call Python function to generate data with a slight delay
|
||||
// to allow the progress bar to start
|
||||
setTimeout(() => {
|
||||
pywebview.api.regenerate_planet(pointsCount, colorScheme)
|
||||
.then(data => {
|
||||
planetData = data;
|
||||
|
||||
// Ensure we're at 100% when done
|
||||
document.getElementById('progress-bar').style.width = '100%';
|
||||
document.getElementById('progress-text').textContent = '100%';
|
||||
|
||||
// Show visualization after a short delay
|
||||
setTimeout(() => {
|
||||
showScreen('visualization-screen');
|
||||
}, 300);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error generating planet:', error);
|
||||
alert('An error occurred while generating the planet. Please try again.');
|
||||
showScreen('settings-screen');
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Set planet data and update visualizations
|
||||
function setPlanetData(data) {
|
||||
planetData = data;
|
||||
|
||||
// Ensure progress is complete and move to visualization
|
||||
document.getElementById('progress-bar').style.width = '100%';
|
||||
document.getElementById('progress-text').textContent = '100%';
|
||||
|
||||
// Clear any progress simulation
|
||||
clearInterval(progressInterval);
|
||||
|
||||
// Show visualization screen after a short delay
|
||||
setTimeout(() => {
|
||||
showScreen('visualization-screen');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
function setupEventListeners() {
|
||||
// Generate button on settings screen
|
||||
document.getElementById('generate-btn').addEventListener('click', generatePlanet);
|
||||
|
||||
// Regenerate button on visualization screen
|
||||
document.getElementById('regenerate-btn').addEventListener('click', () => {
|
||||
const pointsCount = parseInt(document.getElementById('points-count').value);
|
||||
const colorScheme = document.getElementById('color-scheme').value;
|
||||
|
||||
// Show loading screen
|
||||
showScreen('loading-screen');
|
||||
|
||||
// Start progress bar
|
||||
startProgressBar();
|
||||
|
||||
// Call Python function to regenerate data
|
||||
setTimeout(() => {
|
||||
pywebview.api.regenerate_planet(pointsCount, colorScheme)
|
||||
.then(data => {
|
||||
planetData = data;
|
||||
setTimeout(() => {
|
||||
showScreen('visualization-screen');
|
||||
}, 300);
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Back button on visualization screen
|
||||
document.getElementById('back-btn').addEventListener('click', () => {
|
||||
showScreen('settings-screen');
|
||||
});
|
||||
|
||||
// Window resize handler
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
|
||||
// Initialize when document is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupEventListeners();
|
||||
|
||||
// Start on the settings screen
|
||||
showScreen('settings-screen');
|
||||
|
||||
// Start the animation loop anyway for when we need it
|
||||
animate();
|
||||
});
|
||||
243
planet_visualization.py
Normal file
243
planet_visualization.py
Normal file
@ -0,0 +1,243 @@
|
||||
import numpy as np
|
||||
import xarray as xr
|
||||
import random
|
||||
import webview
|
||||
import os
|
||||
import json
|
||||
import pathlib
|
||||
import time
|
||||
|
||||
# Function to generate Fibonacci sphere points
|
||||
def fibonacci_sphere(n_points, randomize=False):
|
||||
"""
|
||||
Parameters:
|
||||
n_points (int): Number of points to generate
|
||||
randomize (bool): Add some randomization to the points
|
||||
|
||||
Returns:
|
||||
ndarray: Array of [x, y, z] coordinates of points on a unit sphere
|
||||
"""
|
||||
points = []
|
||||
phi = np.pi * (3. - np.sqrt(5.)) # golden angle in radians
|
||||
|
||||
for i in range(n_points):
|
||||
y = 1 - (i / float(n_points - 1)) * 2 # y goes from 1 to -1
|
||||
radius = np.sqrt(1 - y * y) # radius at y
|
||||
|
||||
theta = phi * i # golden angle increment
|
||||
|
||||
if randomize:
|
||||
# Add slight randomization to the angle
|
||||
theta = theta + random.uniform(-0.1, 0.1)
|
||||
|
||||
x = np.cos(theta) * radius
|
||||
z = np.sin(theta) * radius
|
||||
|
||||
points.append([x, y, z])
|
||||
|
||||
return np.array(points)
|
||||
|
||||
# Function to convert cartesian to spherical coordinates
|
||||
def cartesian_to_spherical(points):
|
||||
"""
|
||||
Convert cartesian (x, y, z) coordinates to spherical (r, theta, phi) coordinates.
|
||||
|
||||
Parameters:
|
||||
points (ndarray): Array of [x, y, z] coordinates
|
||||
|
||||
Returns:
|
||||
ndarray: Array of [r, theta, phi] coordinates
|
||||
"""
|
||||
x, y, z = points[:, 0], points[:, 1], points[:, 2]
|
||||
r = np.sqrt(x**2 + y**2 + z**2)
|
||||
theta = np.arccos(z / r) # polar angle (0 to pi)
|
||||
phi = np.arctan2(y, x) # azimuthal angle (0 to 2pi)
|
||||
|
||||
return np.column_stack((r, theta, phi))
|
||||
|
||||
# Function to generate random colors for points
|
||||
def generate_point_colors(n_points, color_scheme='random'):
|
||||
"""
|
||||
Generate colors for each point.
|
||||
|
||||
Parameters:
|
||||
n_points (int): Number of points
|
||||
color_scheme (str): Type of color scheme to use
|
||||
|
||||
Returns:
|
||||
ndarray: Array of [r, g, b] values (0-1 scale)
|
||||
"""
|
||||
colors = []
|
||||
|
||||
if color_scheme == 'random':
|
||||
for _ in range(n_points):
|
||||
colors.append([random.random(), random.random(), random.random()])
|
||||
|
||||
elif color_scheme == 'terrain':
|
||||
# Generate terrain-like colors
|
||||
for _ in range(n_points):
|
||||
elev = random.random() # simulated elevation
|
||||
if elev < 0.25:
|
||||
# Deep water (dark blue)
|
||||
colors.append([0.0, 0.0, 0.5 + 0.5 * random.random()])
|
||||
elif elev < 0.4:
|
||||
# Shallow water (light blue)
|
||||
colors.append([0.0, 0.3 + 0.3 * random.random(), 0.8])
|
||||
elif elev < 0.5:
|
||||
# Sand (tan)
|
||||
colors.append([0.76, 0.7, 0.5])
|
||||
elif elev < 0.7:
|
||||
# Grass/forest (green)
|
||||
colors.append([0.0, 0.5 + 0.3 * random.random(), 0.0])
|
||||
elif elev < 0.9:
|
||||
# Mountain (gray/brown)
|
||||
g = 0.3 + 0.2 * random.random()
|
||||
colors.append([g, g, g])
|
||||
else:
|
||||
# Snow (white)
|
||||
colors.append([0.9, 0.9, 0.9])
|
||||
|
||||
return np.array(colors)
|
||||
|
||||
# Generate point data and store in xarray dataset
|
||||
def create_planet_data(n_points=10000, color_scheme='terrain'):
|
||||
"""
|
||||
Create planet data with Fibonacci sphere distribution and colors.
|
||||
|
||||
Parameters:
|
||||
n_points (int): Number of points to generate
|
||||
color_scheme (str): Type of color scheme to use
|
||||
|
||||
Returns:
|
||||
xarray.Dataset: Dataset containing planet data
|
||||
"""
|
||||
# Generate Fibonacci sphere points
|
||||
cartesian_points = fibonacci_sphere(n_points)
|
||||
|
||||
# Add some small delays for larger point sets to make progress bar meaningful
|
||||
if n_points > 5000:
|
||||
time.sleep(0.2) # Simulate computation time
|
||||
|
||||
# Convert to spherical coordinates
|
||||
spherical_points = cartesian_to_spherical(cartesian_points)
|
||||
|
||||
if n_points > 5000:
|
||||
time.sleep(0.2) # Simulate computation time
|
||||
|
||||
# Generate colors
|
||||
colors = generate_point_colors(n_points, color_scheme)
|
||||
|
||||
if n_points > 5000:
|
||||
time.sleep(0.2) # Simulate computation time
|
||||
|
||||
# Create xarray dataset
|
||||
ds = xr.Dataset(
|
||||
data_vars={
|
||||
'x': ('point_idx', cartesian_points[:, 0]),
|
||||
'y': ('point_idx', cartesian_points[:, 1]),
|
||||
'z': ('point_idx', cartesian_points[:, 2]),
|
||||
'r': ('point_idx', spherical_points[:, 0]),
|
||||
'theta': ('point_idx', spherical_points[:, 1]),
|
||||
'phi': ('point_idx', spherical_points[:, 2]),
|
||||
'color_r': ('point_idx', colors[:, 0]),
|
||||
'color_g': ('point_idx', colors[:, 1]),
|
||||
'color_b': ('point_idx', colors[:, 2]),
|
||||
},
|
||||
coords={
|
||||
'point_idx': np.arange(n_points),
|
||||
},
|
||||
attrs={
|
||||
'description': 'Planet data with Fibonacci sphere distribution',
|
||||
'n_points': n_points,
|
||||
'color_scheme': color_scheme,
|
||||
}
|
||||
)
|
||||
|
||||
return ds
|
||||
|
||||
# Function to prepare data for visualization
|
||||
def prepare_visualization_data(ds):
|
||||
"""
|
||||
Prepare the xarray dataset for visualization in the web view.
|
||||
|
||||
Parameters:
|
||||
ds (xarray.Dataset): Dataset containing planet data
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with data for visualization
|
||||
"""
|
||||
# Extract data for visualization
|
||||
point_data = []
|
||||
|
||||
for i in range(len(ds.point_idx)):
|
||||
point_data.append({
|
||||
'x': float(ds.x[i].values),
|
||||
'y': float(ds.y[i].values),
|
||||
'z': float(ds.z[i].values),
|
||||
'theta': float(ds.theta[i].values),
|
||||
'phi': float(ds.phi[i].values),
|
||||
'color': [
|
||||
float(ds.color_r[i].values),
|
||||
float(ds.color_g[i].values),
|
||||
float(ds.color_b[i].values)
|
||||
]
|
||||
})
|
||||
|
||||
return {'points': point_data}
|
||||
|
||||
# API exposed to the browser
|
||||
class Api:
|
||||
def __init__(self, ds):
|
||||
self.ds = ds
|
||||
|
||||
def regenerate_planet(self, n_points, color_scheme):
|
||||
"""
|
||||
Regenerate planet data with new parameters.
|
||||
|
||||
Parameters:
|
||||
n_points (int): Number of points
|
||||
color_scheme (str): Color scheme to use
|
||||
|
||||
Returns:
|
||||
dict: Updated visualization data
|
||||
"""
|
||||
self.ds = create_planet_data(n_points, color_scheme)
|
||||
return prepare_visualization_data(self.ds)
|
||||
|
||||
def main():
|
||||
# Create planet data
|
||||
n_points = 5000 # default number of points
|
||||
color_scheme = 'terrain' # default color scheme
|
||||
|
||||
ds = create_planet_data(n_points, color_scheme)
|
||||
|
||||
# Get the directory of the current script
|
||||
script_dir = pathlib.Path(__file__).parent.absolute()
|
||||
|
||||
# Prepare data for visualization
|
||||
data = prepare_visualization_data(ds)
|
||||
|
||||
# Create API instance
|
||||
api = Api(ds)
|
||||
|
||||
# Function to initialize data after window loads
|
||||
def on_loaded():
|
||||
window.evaluate_js(f'setPlanetData({json.dumps(data)})')
|
||||
|
||||
# Create webview window
|
||||
window = webview.create_window(
|
||||
'Planet Visualization',
|
||||
url=os.path.join(script_dir, 'index.html'),
|
||||
js_api=api,
|
||||
width=1200,
|
||||
height=800
|
||||
)
|
||||
|
||||
# Set up the loaded event handler
|
||||
window.events.loaded += on_loaded
|
||||
|
||||
# Start webview
|
||||
webview.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
242
styles.css
Normal file
242
styles.css
Normal file
@ -0,0 +1,242 @@
|
||||
/* Basic Reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Screen Management */
|
||||
.screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.screen.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Settings Screen */
|
||||
h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background-color: #4285f4;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
background-color: #3367d6;
|
||||
}
|
||||
|
||||
/* Loading Screen */
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 20px;
|
||||
background-color: #eee;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: #4285f4;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
#progress-text {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Visualization Screen */
|
||||
#vis-controls {
|
||||
padding: 12px 20px;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ddd;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#vis-controls button {
|
||||
padding: 8px 12px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#back-btn {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#regenerate-btn {
|
||||
background-color: #4285f4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#vis-controls button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#vis-controls label {
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Main Visualization Area */
|
||||
#visualization {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: calc(100% - 50px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#threejs-container, #d3-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#threejs-container {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
#d3-container {
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* D3 Map Specific Styles */
|
||||
.map-label {
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.graticule {
|
||||
fill: none;
|
||||
stroke: rgba(255, 255, 255, 0.5);
|
||||
stroke-width: 0.5px;
|
||||
}
|
||||
|
||||
.equator {
|
||||
fill: none;
|
||||
stroke: #00ffff;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.prime-meridian {
|
||||
fill: none;
|
||||
stroke: #ff0000;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.continent-outline {
|
||||
fill: #d3b683;
|
||||
stroke: #a89070;
|
||||
stroke-width: 0.5px;
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.planet-point {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
#visualization {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#threejs-container, #d3-container {
|
||||
flex: 1;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
#d3-container {
|
||||
border-left: none;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
#vis-controls {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#vis-controls > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user