diff --git a/index.html b/index.html new file mode 100644 index 0000000..14d87c1 --- /dev/null +++ b/index.html @@ -0,0 +1,68 @@ + + + + + + Planet Visualization + + + + + + + + + + + +
+
+

Planet Generator

+
+
+ + +
+
+ + +
+ +
+
+
+ + +
+
+

Generating Planet...

+
+
+
+
0%
+
+
+ + +
+
+
+ + + + +
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/planet_visualization.js b/planet_visualization.js new file mode 100644 index 0000000..78d613d --- /dev/null +++ b/planet_visualization.js @@ -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(); +}); \ No newline at end of file diff --git a/planet_visualization.py b/planet_visualization.py new file mode 100644 index 0000000..a2bb10d --- /dev/null +++ b/planet_visualization.py @@ -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() \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..04d8946 --- /dev/null +++ b/styles.css @@ -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; + } +} \ No newline at end of file