Spaces:
Running
Running
Upload 18 files
Browse files- .gitignore +21 -0
- DEPLOYMENT.md +111 -0
- DataManager.js +58 -0
- HUGGINGFACE_SETUP.md +117 -0
- InteractionController.js +90 -0
- LICENSE +21 -0
- PeriodicTableComponent.js +124 -0
- README.md +59 -11
- SceneManager.js +116 -0
- SpectrumDisplayComponent.js +133 -0
- all-elements-data.js +25 -0
- element-science-data.txt +31 -0
- elementLayout.js +48 -0
- index.html +0 -0
- main.js +66 -0
- spectral-data.json +118 -0
- styles.css +122 -0
- wavelengthUtils.js +68 -0
.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IDE
|
| 2 |
+
.vscode/
|
| 3 |
+
.idea/
|
| 4 |
+
*.swp
|
| 5 |
+
*.swo
|
| 6 |
+
*~
|
| 7 |
+
|
| 8 |
+
# OS
|
| 9 |
+
.DS_Store
|
| 10 |
+
Thumbs.db
|
| 11 |
+
|
| 12 |
+
# Kiro
|
| 13 |
+
.kiro/
|
| 14 |
+
|
| 15 |
+
# Logs
|
| 16 |
+
*.log
|
| 17 |
+
npm-debug.log*
|
| 18 |
+
|
| 19 |
+
# Temporary files
|
| 20 |
+
*.tmp
|
| 21 |
+
*.temp
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deploying to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
## Quick Start
|
| 4 |
+
|
| 5 |
+
1. **Create a Hugging Face Account**
|
| 6 |
+
- Go to https://huggingface.co/join
|
| 7 |
+
- Sign up for a free account
|
| 8 |
+
|
| 9 |
+
2. **Create a New Space**
|
| 10 |
+
- Go to https://huggingface.co/spaces
|
| 11 |
+
- Click "Create new Space"
|
| 12 |
+
- Choose a name for your space (e.g., "periodic-table-viewer")
|
| 13 |
+
- Select SDK: **Static**
|
| 14 |
+
- Choose visibility: Public or Private
|
| 15 |
+
- Click "Create Space"
|
| 16 |
+
|
| 17 |
+
3. **Upload Files**
|
| 18 |
+
|
| 19 |
+
**Option A: Using Git (Recommended)**
|
| 20 |
+
```bash
|
| 21 |
+
# Clone your space repository
|
| 22 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 23 |
+
cd YOUR_SPACE_NAME
|
| 24 |
+
|
| 25 |
+
# Copy all files from this project
|
| 26 |
+
cp /path/to/this/project/index.html .
|
| 27 |
+
cp /path/to/this/project/README.md .
|
| 28 |
+
|
| 29 |
+
# Commit and push
|
| 30 |
+
git add .
|
| 31 |
+
git commit -m "Initial commit: Periodic Table Spectrum Viewer"
|
| 32 |
+
git push
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
**Option B: Using Web Interface**
|
| 36 |
+
- In your Space, click "Files" tab
|
| 37 |
+
- Click "Add file" → "Upload files"
|
| 38 |
+
- Upload `index.html` and `README.md`
|
| 39 |
+
- Click "Commit changes to main"
|
| 40 |
+
|
| 41 |
+
4. **Wait for Build**
|
| 42 |
+
- Hugging Face will automatically build and deploy your space
|
| 43 |
+
- This usually takes 10-30 seconds
|
| 44 |
+
- Your space will be available at: `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME`
|
| 45 |
+
|
| 46 |
+
## Files Needed for Deployment
|
| 47 |
+
|
| 48 |
+
Only these files are required:
|
| 49 |
+
- ✅ `index.html` - Main application (self-contained)
|
| 50 |
+
- ✅ `README.md` - Space description and metadata
|
| 51 |
+
|
| 52 |
+
**NOT needed** (already embedded in index.html):
|
| 53 |
+
- ❌ JavaScript files (all code is in index.html)
|
| 54 |
+
- ❌ CSS files (all styles are in index.html)
|
| 55 |
+
- ❌ Data files (all data is in index.html)
|
| 56 |
+
|
| 57 |
+
## Customization
|
| 58 |
+
|
| 59 |
+
### Update Space Metadata
|
| 60 |
+
Edit the frontmatter in `README.md`:
|
| 61 |
+
```yaml
|
| 62 |
+
---
|
| 63 |
+
title: Your Custom Title
|
| 64 |
+
emoji: 🔬 # Choose any emoji
|
| 65 |
+
colorFrom: blue # Start color for gradient
|
| 66 |
+
colorTo: purple # End color for gradient
|
| 67 |
+
sdk: static # Keep as "static"
|
| 68 |
+
pinned: false # Pin to your profile
|
| 69 |
+
license: mit # Choose your license
|
| 70 |
+
---
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Update Credits
|
| 74 |
+
In `index.html`, find the footer section and update:
|
| 75 |
+
```html
|
| 76 |
+
<footer>
|
| 77 |
+
<p>Created by Your Name | Data from <a href="..." target="_blank">Source</a></p>
|
| 78 |
+
</footer>
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
## Troubleshooting
|
| 82 |
+
|
| 83 |
+
### Space not loading?
|
| 84 |
+
- Check the "Logs" tab in your Space for errors
|
| 85 |
+
- Ensure `index.html` is in the root directory
|
| 86 |
+
- Verify the README.md has correct frontmatter
|
| 87 |
+
|
| 88 |
+
### Three.js not loading?
|
| 89 |
+
- The app uses CDN links for Three.js
|
| 90 |
+
- Ensure you have internet connection
|
| 91 |
+
- Check browser console for errors
|
| 92 |
+
|
| 93 |
+
### Need to update?
|
| 94 |
+
```bash
|
| 95 |
+
# Make changes to index.html locally
|
| 96 |
+
# Then push updates
|
| 97 |
+
git add index.html
|
| 98 |
+
git commit -m "Update: description of changes"
|
| 99 |
+
git push
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
## Support
|
| 103 |
+
|
| 104 |
+
For issues with:
|
| 105 |
+
- **Hugging Face Spaces**: https://huggingface.co/docs/hub/spaces
|
| 106 |
+
- **This Project**: Open an issue in the repository
|
| 107 |
+
|
| 108 |
+
## Example Spaces
|
| 109 |
+
|
| 110 |
+
See other static spaces for inspiration:
|
| 111 |
+
- https://huggingface.co/spaces (search for "static" SDK)
|
DataManager.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export class DataManager {
|
| 2 |
+
constructor() {
|
| 3 |
+
this.elementsData = new Map();
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
async loadSpectralData(dataUrl) {
|
| 7 |
+
try {
|
| 8 |
+
const response = await fetch(dataUrl);
|
| 9 |
+
if (!response.ok) {
|
| 10 |
+
throw new Error(`Failed to load spectral data: ${response.statusText}`);
|
| 11 |
+
}
|
| 12 |
+
const data = await response.json();
|
| 13 |
+
|
| 14 |
+
if (!this.validateData(data)) {
|
| 15 |
+
throw new Error('Invalid spectral data format');
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Store elements in a Map for quick lookup
|
| 19 |
+
data.elements.forEach(element => {
|
| 20 |
+
this.elementsData.set(element.symbol, element);
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
return true;
|
| 24 |
+
} catch (error) {
|
| 25 |
+
console.error('Error loading spectral data:', error);
|
| 26 |
+
throw error;
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
getElementData(symbol) {
|
| 31 |
+
return this.elementsData.get(symbol) || null;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
getAllElements() {
|
| 35 |
+
return Array.from(this.elementsData.keys());
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
validateData(data) {
|
| 39 |
+
if (!data || !Array.isArray(data.elements)) {
|
| 40 |
+
return false;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
for (const element of data.elements) {
|
| 44 |
+
if (!element.symbol || !element.name || !element.atomicNumber) {
|
| 45 |
+
return false;
|
| 46 |
+
}
|
| 47 |
+
if (!Array.isArray(element.spectralLines)) {
|
| 48 |
+
return false;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return true;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
validateWavelength(wavelength) {
|
| 56 |
+
return wavelength >= 10 && wavelength <= 10000;
|
| 57 |
+
}
|
| 58 |
+
}
|
HUGGINGFACE_SETUP.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces Setup - Quick Guide
|
| 2 |
+
|
| 3 |
+
## ✅ Your Project is Ready!
|
| 4 |
+
|
| 5 |
+
Your Periodic Table Spectrum Viewer is now configured for Hugging Face Spaces deployment.
|
| 6 |
+
|
| 7 |
+
## 📦 What's Been Prepared
|
| 8 |
+
|
| 9 |
+
1. **README.md** - Contains Hugging Face Space metadata and project description
|
| 10 |
+
2. **.gitignore** - Excludes unnecessary files from version control
|
| 11 |
+
3. **DEPLOYMENT.md** - Detailed deployment instructions
|
| 12 |
+
4. **index.html** - Self-contained application (no external dependencies needed)
|
| 13 |
+
|
| 14 |
+
## 🚀 Deploy in 3 Steps
|
| 15 |
+
|
| 16 |
+
### Step 1: Create Space
|
| 17 |
+
1. Go to https://huggingface.co/spaces
|
| 18 |
+
2. Click "Create new Space"
|
| 19 |
+
3. Name: `periodic-table-viewer` (or your choice)
|
| 20 |
+
4. SDK: Select **"Static"**
|
| 21 |
+
5. Click "Create Space"
|
| 22 |
+
|
| 23 |
+
### Step 2: Upload Files
|
| 24 |
+
Upload only these 2 files:
|
| 25 |
+
- `index.html`
|
| 26 |
+
- `README.md`
|
| 27 |
+
|
| 28 |
+
### Step 3: Done!
|
| 29 |
+
Your space will be live at:
|
| 30 |
+
`https://huggingface.co/spaces/YOUR_USERNAME/periodic-table-viewer`
|
| 31 |
+
|
| 32 |
+
## 📝 Important Notes
|
| 33 |
+
|
| 34 |
+
### ✅ What's Included
|
| 35 |
+
- All JavaScript code (embedded in index.html)
|
| 36 |
+
- All CSS styles (embedded in index.html)
|
| 37 |
+
- All element data (embedded in index.html)
|
| 38 |
+
- Three.js library (loaded from CDN)
|
| 39 |
+
|
| 40 |
+
### ❌ What's NOT Needed
|
| 41 |
+
- No separate .js files
|
| 42 |
+
- No separate .css files
|
| 43 |
+
- No data files
|
| 44 |
+
- No build process
|
| 45 |
+
- No dependencies to install
|
| 46 |
+
|
| 47 |
+
### 🌐 External Dependencies
|
| 48 |
+
The app loads Three.js from CDN:
|
| 49 |
+
- `https://cdn.jsdelivr.net/npm/three@0.160.0/`
|
| 50 |
+
|
| 51 |
+
This is standard and works perfectly on Hugging Face Spaces.
|
| 52 |
+
|
| 53 |
+
## 🎨 Customization
|
| 54 |
+
|
| 55 |
+
### Change Space Appearance
|
| 56 |
+
Edit `README.md` frontmatter:
|
| 57 |
+
```yaml
|
| 58 |
+
---
|
| 59 |
+
title: Your Title Here
|
| 60 |
+
emoji: 🔬
|
| 61 |
+
colorFrom: blue
|
| 62 |
+
colorTo: purple
|
| 63 |
+
---
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Change Credits
|
| 67 |
+
Edit the footer in `index.html`:
|
| 68 |
+
```html
|
| 69 |
+
<footer>
|
| 70 |
+
<p>Created by Your Name</p>
|
| 71 |
+
</footer>
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 🔧 Testing Locally
|
| 75 |
+
|
| 76 |
+
Before deploying, test locally:
|
| 77 |
+
1. Open `index.html` in a web browser
|
| 78 |
+
2. Check that all features work:
|
| 79 |
+
- 3D periodic table loads
|
| 80 |
+
- Elements are clickable
|
| 81 |
+
- Spectrum popup works
|
| 82 |
+
- All 118 elements have data
|
| 83 |
+
|
| 84 |
+
## 📚 Resources
|
| 85 |
+
|
| 86 |
+
- **Hugging Face Spaces Docs**: https://huggingface.co/docs/hub/spaces
|
| 87 |
+
- **Static Spaces Guide**: https://huggingface.co/docs/hub/spaces-sdks-static
|
| 88 |
+
- **Example Spaces**: Search "static" on https://huggingface.co/spaces
|
| 89 |
+
|
| 90 |
+
## 🆘 Troubleshooting
|
| 91 |
+
|
| 92 |
+
**Space shows blank page?**
|
| 93 |
+
- Check browser console for errors
|
| 94 |
+
- Verify index.html is in root directory
|
| 95 |
+
- Ensure README.md has correct frontmatter
|
| 96 |
+
|
| 97 |
+
**Three.js not loading?**
|
| 98 |
+
- CDN might be temporarily down
|
| 99 |
+
- Check internet connection
|
| 100 |
+
- Try refreshing the page
|
| 101 |
+
|
| 102 |
+
**Need help?**
|
| 103 |
+
- Check DEPLOYMENT.md for detailed instructions
|
| 104 |
+
- Visit Hugging Face Discord: https://hf.co/join/discord
|
| 105 |
+
|
| 106 |
+
## 🎉 Next Steps
|
| 107 |
+
|
| 108 |
+
1. Deploy to Hugging Face Spaces
|
| 109 |
+
2. Share your space URL
|
| 110 |
+
3. Get feedback from users
|
| 111 |
+
4. Consider adding more features:
|
| 112 |
+
- Search functionality
|
| 113 |
+
- Favorite elements
|
| 114 |
+
- Compare elements side-by-side
|
| 115 |
+
- Export spectrum images
|
| 116 |
+
|
| 117 |
+
Good luck with your deployment! 🚀
|
InteractionController.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 2 |
+
|
| 3 |
+
export class InteractionController {
|
| 4 |
+
constructor(sceneManager, periodicTable, spectrumDisplay) {
|
| 5 |
+
this.sceneManager = sceneManager;
|
| 6 |
+
this.periodicTable = periodicTable;
|
| 7 |
+
this.spectrumDisplay = spectrumDisplay;
|
| 8 |
+
this.controls = null;
|
| 9 |
+
this.renderer = sceneManager.getRenderer();
|
| 10 |
+
this.camera = sceneManager.getCamera();
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
initialize() {
|
| 14 |
+
// Set up OrbitControls
|
| 15 |
+
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
| 16 |
+
this.controls.enableDamping = true;
|
| 17 |
+
this.controls.dampingFactor = 0.05;
|
| 18 |
+
this.controls.minDistance = 10;
|
| 19 |
+
this.controls.maxDistance = 50;
|
| 20 |
+
this.controls.enablePan = true;
|
| 21 |
+
|
| 22 |
+
// Mouse move for hover
|
| 23 |
+
this.renderer.domElement.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
| 24 |
+
|
| 25 |
+
// Click for selection
|
| 26 |
+
this.renderer.domElement.addEventListener('click', (e) => this.onClick(e));
|
| 27 |
+
|
| 28 |
+
// ESC key for deselection
|
| 29 |
+
document.addEventListener('keydown', (e) => {
|
| 30 |
+
if (e.key === 'Escape') {
|
| 31 |
+
this.deselectElement();
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Update controls in animation loop
|
| 36 |
+
this.updateControls();
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
onMouseMove(event) {
|
| 40 |
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
| 41 |
+
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
| 42 |
+
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
| 43 |
+
|
| 44 |
+
const element = this.periodicTable.getElementAtPosition(x, y, this.camera);
|
| 45 |
+
|
| 46 |
+
if (element) {
|
| 47 |
+
this.renderer.domElement.style.cursor = 'pointer';
|
| 48 |
+
this.periodicTable.highlightElement(element);
|
| 49 |
+
} else {
|
| 50 |
+
this.renderer.domElement.style.cursor = 'default';
|
| 51 |
+
this.periodicTable.highlightElement(null);
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
onClick(event) {
|
| 56 |
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
| 57 |
+
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
| 58 |
+
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
| 59 |
+
|
| 60 |
+
const element = this.periodicTable.getElementAtPosition(x, y, this.camera);
|
| 61 |
+
|
| 62 |
+
if (element) {
|
| 63 |
+
this.periodicTable.selectElement(element);
|
| 64 |
+
this.spectrumDisplay.showSpectrum(element);
|
| 65 |
+
} else {
|
| 66 |
+
this.deselectElement();
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
deselectElement() {
|
| 71 |
+
this.periodicTable.selectElement(null);
|
| 72 |
+
this.spectrumDisplay.hideSpectrum();
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
updateControls() {
|
| 76 |
+
const animate = () => {
|
| 77 |
+
requestAnimationFrame(animate);
|
| 78 |
+
if (this.controls) {
|
| 79 |
+
this.controls.update();
|
| 80 |
+
}
|
| 81 |
+
};
|
| 82 |
+
animate();
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
dispose() {
|
| 86 |
+
if (this.controls) {
|
| 87 |
+
this.controls.dispose();
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Andy Kong
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
PeriodicTableComponent.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'three';
|
| 2 |
+
import { getElementPosition, ELEMENT_COLORS } from './elementLayout.js';
|
| 3 |
+
|
| 4 |
+
export class PeriodicTableComponent {
|
| 5 |
+
constructor(scene, dataManager) {
|
| 6 |
+
this.scene = scene;
|
| 7 |
+
this.dataManager = dataManager;
|
| 8 |
+
this.elementMeshes = new Map();
|
| 9 |
+
this.highlightedElement = null;
|
| 10 |
+
this.selectedElement = null;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
createPeriodicTable() {
|
| 14 |
+
const elements = this.dataManager.getAllElements();
|
| 15 |
+
|
| 16 |
+
elements.forEach(symbol => {
|
| 17 |
+
const elementData = this.dataManager.getElementData(symbol);
|
| 18 |
+
const position = getElementPosition(symbol);
|
| 19 |
+
|
| 20 |
+
if (!position) return;
|
| 21 |
+
|
| 22 |
+
// Create element tile
|
| 23 |
+
const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.1);
|
| 24 |
+
const color = ELEMENT_COLORS[elementData.category] || ELEMENT_COLORS.unknown;
|
| 25 |
+
const material = new THREE.MeshStandardMaterial({
|
| 26 |
+
color: color,
|
| 27 |
+
emissive: 0x000000,
|
| 28 |
+
metalness: 0.3,
|
| 29 |
+
roughness: 0.7
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const mesh = new THREE.Mesh(geometry, material);
|
| 33 |
+
mesh.position.set(position.x, position.y, position.z);
|
| 34 |
+
mesh.userData = { symbol, elementData };
|
| 35 |
+
|
| 36 |
+
this.scene.add(mesh);
|
| 37 |
+
this.elementMeshes.set(symbol, mesh);
|
| 38 |
+
|
| 39 |
+
// Add text label
|
| 40 |
+
this.addTextLabel(symbol, position);
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
addTextLabel(symbol, position) {
|
| 45 |
+
const canvas = document.createElement('canvas');
|
| 46 |
+
const context = canvas.getContext('2d');
|
| 47 |
+
canvas.width = 128;
|
| 48 |
+
canvas.height = 128;
|
| 49 |
+
|
| 50 |
+
context.fillStyle = 'white';
|
| 51 |
+
context.font = 'bold 60px Arial';
|
| 52 |
+
context.textAlign = 'center';
|
| 53 |
+
context.textBaseline = 'middle';
|
| 54 |
+
context.fillText(symbol, 64, 64);
|
| 55 |
+
|
| 56 |
+
const texture = new THREE.CanvasTexture(canvas);
|
| 57 |
+
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
|
| 58 |
+
const sprite = new THREE.Sprite(spriteMaterial);
|
| 59 |
+
sprite.scale.set(0.6, 0.6, 1);
|
| 60 |
+
sprite.position.set(position.x, position.y, position.z + 0.1);
|
| 61 |
+
|
| 62 |
+
this.scene.add(sprite);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
highlightElement(symbol) {
|
| 66 |
+
// Reset previous highlight
|
| 67 |
+
if (this.highlightedElement && this.highlightedElement !== this.selectedElement) {
|
| 68 |
+
const prevMesh = this.elementMeshes.get(this.highlightedElement);
|
| 69 |
+
if (prevMesh) {
|
| 70 |
+
prevMesh.material.emissive.setHex(0x000000);
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Apply new highlight
|
| 75 |
+
if (symbol && symbol !== this.selectedElement) {
|
| 76 |
+
const mesh = this.elementMeshes.get(symbol);
|
| 77 |
+
if (mesh) {
|
| 78 |
+
mesh.material.emissive.setHex(0x444444);
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
this.highlightedElement = symbol;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
selectElement(symbol) {
|
| 86 |
+
// Reset previous selection
|
| 87 |
+
if (this.selectedElement) {
|
| 88 |
+
const prevMesh = this.elementMeshes.get(this.selectedElement);
|
| 89 |
+
if (prevMesh) {
|
| 90 |
+
prevMesh.material.emissive.setHex(0x000000);
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Apply new selection
|
| 95 |
+
if (symbol) {
|
| 96 |
+
const mesh = this.elementMeshes.get(symbol);
|
| 97 |
+
if (mesh) {
|
| 98 |
+
mesh.material.emissive.setHex(0x00ff00);
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
this.selectedElement = symbol;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
getElementAtPosition(x, y, camera) {
|
| 106 |
+
const raycaster = new THREE.Raycaster();
|
| 107 |
+
const mouse = new THREE.Vector2(x, y);
|
| 108 |
+
|
| 109 |
+
raycaster.setFromCamera(mouse, camera);
|
| 110 |
+
|
| 111 |
+
const meshes = Array.from(this.elementMeshes.values());
|
| 112 |
+
const intersects = raycaster.intersectObjects(meshes);
|
| 113 |
+
|
| 114 |
+
if (intersects.length > 0) {
|
| 115 |
+
return intersects[0].object.userData.symbol;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
return null;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
getSelectedElement() {
|
| 122 |
+
return this.selectedElement;
|
| 123 |
+
}
|
| 124 |
+
}
|
README.md
CHANGED
|
@@ -1,11 +1,59 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Periodic Table
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: static
|
| 7 |
-
pinned: false
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Periodic Table Spectrum Viewer
|
| 3 |
+
emoji: 🔬
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: static
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Periodic Table Spectrum Viewer
|
| 12 |
+
|
| 13 |
+
An interactive 3D periodic table with detailed emission spectrum visualization for all 118 elements.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
|
| 17 |
+
- **3D Interactive Periodic Table**: Explore all 118 elements in a beautiful 3D layout using Three.js
|
| 18 |
+
- **Emission Spectra**: View accurate emission spectra for each element
|
| 19 |
+
- **Detailed Spectrum Viewer**: Click on any spectrum to see an expanded view with:
|
| 20 |
+
- Visible spectrum (380-750nm) with continuous color gradient
|
| 21 |
+
- UV and IR regions for elements with lines outside visible range
|
| 22 |
+
- Electron transition information
|
| 23 |
+
- Wavelength labels and intensity data
|
| 24 |
+
- **Element Information**:
|
| 25 |
+
- Cosmic origin (how the element was created in the universe)
|
| 26 |
+
- Chemistry properties and uses
|
| 27 |
+
- Physics data (electron configuration, isotopes)
|
| 28 |
+
- **Color-Coded Categories**: Elements colored by type (metals, nonmetals, noble gases, etc.)
|
| 29 |
+
- **Hover Effects**: Elements pop forward when you hover over them
|
| 30 |
+
- **Collapsible Info Panel**: Full-height panel with collapsible sections
|
| 31 |
+
|
| 32 |
+
## How to Use
|
| 33 |
+
|
| 34 |
+
1. **Rotate**: Click and drag to rotate the periodic table
|
| 35 |
+
2. **Zoom**: Scroll to zoom in/out
|
| 36 |
+
3. **Select Element**: Click on any element to view its details
|
| 37 |
+
4. **View Detailed Spectrum**: Click on the emission spectrum graph to open a full-width detailed view
|
| 38 |
+
5. **Deselect**: Press ESC or click away to deselect
|
| 39 |
+
|
| 40 |
+
## Data Sources
|
| 41 |
+
|
| 42 |
+
- Spectral data from [NIST Atomic Spectra Database](https://www.nist.gov/pml/atomic-spectra-database)
|
| 43 |
+
- Cosmic origin information based on astrophysics research
|
| 44 |
+
- Chemistry and physics data from scientific databases
|
| 45 |
+
|
| 46 |
+
## Technology
|
| 47 |
+
|
| 48 |
+
- **Three.js**: 3D visualization
|
| 49 |
+
- **Vanilla JavaScript**: No framework dependencies
|
| 50 |
+
- **Canvas API**: Spectrum rendering
|
| 51 |
+
- **CSS3**: Modern styling and animations
|
| 52 |
+
|
| 53 |
+
## Credits
|
| 54 |
+
|
| 55 |
+
Created by Andy Kong
|
| 56 |
+
|
| 57 |
+
## License
|
| 58 |
+
|
| 59 |
+
MIT License - Feel free to use and modify for your own projects!
|
SceneManager.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'three';
|
| 2 |
+
|
| 3 |
+
export class SceneManager {
|
| 4 |
+
constructor(container) {
|
| 5 |
+
this.container = container;
|
| 6 |
+
this.scene = null;
|
| 7 |
+
this.camera = null;
|
| 8 |
+
this.renderer = null;
|
| 9 |
+
this.animationId = null;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
initialize() {
|
| 13 |
+
// Check WebGL support
|
| 14 |
+
if (!this.checkWebGLSupport()) {
|
| 15 |
+
this.showWebGLError();
|
| 16 |
+
return false;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Create scene
|
| 20 |
+
this.scene = new THREE.Scene();
|
| 21 |
+
this.scene.background = new THREE.Color(0x1a1a2e);
|
| 22 |
+
|
| 23 |
+
// Create camera
|
| 24 |
+
const aspect = this.container.clientWidth / this.container.clientHeight;
|
| 25 |
+
this.camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
|
| 26 |
+
this.camera.position.set(0, 0, 30);
|
| 27 |
+
|
| 28 |
+
// Create renderer
|
| 29 |
+
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 30 |
+
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
|
| 31 |
+
this.renderer.setPixelRatio(window.devicePixelRatio);
|
| 32 |
+
this.container.appendChild(this.renderer.domElement);
|
| 33 |
+
|
| 34 |
+
// Add lights
|
| 35 |
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
| 36 |
+
this.scene.add(ambientLight);
|
| 37 |
+
|
| 38 |
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
| 39 |
+
directionalLight.position.set(10, 10, 10);
|
| 40 |
+
this.scene.add(directionalLight);
|
| 41 |
+
|
| 42 |
+
// Handle window resize
|
| 43 |
+
window.addEventListener('resize', () => this.onWindowResize());
|
| 44 |
+
|
| 45 |
+
return true;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
checkWebGLSupport() {
|
| 49 |
+
try {
|
| 50 |
+
const canvas = document.createElement('canvas');
|
| 51 |
+
return !!(window.WebGLRenderingContext &&
|
| 52 |
+
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
|
| 53 |
+
} catch (e) {
|
| 54 |
+
return false;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
showWebGLError() {
|
| 59 |
+
const errorDiv = document.createElement('div');
|
| 60 |
+
errorDiv.style.cssText = `
|
| 61 |
+
position: fixed;
|
| 62 |
+
top: 50%;
|
| 63 |
+
left: 50%;
|
| 64 |
+
transform: translate(-50%, -50%);
|
| 65 |
+
background: rgba(0, 0, 0, 0.9);
|
| 66 |
+
color: white;
|
| 67 |
+
padding: 30px;
|
| 68 |
+
border-radius: 10px;
|
| 69 |
+
text-align: center;
|
| 70 |
+
z-index: 10000;
|
| 71 |
+
`;
|
| 72 |
+
errorDiv.innerHTML = `
|
| 73 |
+
<h2>WebGL Not Available</h2>
|
| 74 |
+
<p>WebGL is required but not available. Please use a modern browser with WebGL support.</p>
|
| 75 |
+
<p><a href="https://get.webgl.org/" target="_blank" style="color: #4fc3f7;">Learn more</a></p>
|
| 76 |
+
`;
|
| 77 |
+
document.body.appendChild(errorDiv);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
start() {
|
| 81 |
+
if (!this.renderer) return;
|
| 82 |
+
|
| 83 |
+
const animate = () => {
|
| 84 |
+
this.animationId = requestAnimationFrame(animate);
|
| 85 |
+
this.renderer.render(this.scene, this.camera);
|
| 86 |
+
};
|
| 87 |
+
animate();
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
stop() {
|
| 91 |
+
if (this.animationId) {
|
| 92 |
+
cancelAnimationFrame(this.animationId);
|
| 93 |
+
this.animationId = null;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
onWindowResize() {
|
| 98 |
+
if (!this.camera || !this.renderer) return;
|
| 99 |
+
|
| 100 |
+
this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
|
| 101 |
+
this.camera.updateProjectionMatrix();
|
| 102 |
+
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
getScene() {
|
| 106 |
+
return this.scene;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
getCamera() {
|
| 110 |
+
return this.camera;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
getRenderer() {
|
| 114 |
+
return this.renderer;
|
| 115 |
+
}
|
| 116 |
+
}
|
SpectrumDisplayComponent.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getSpectrumColor } from './wavelengthUtils.js';
|
| 2 |
+
|
| 3 |
+
export class SpectrumDisplayComponent {
|
| 4 |
+
constructor(dataManager) {
|
| 5 |
+
this.dataManager = dataManager;
|
| 6 |
+
this.infoPanel = document.getElementById('info-panel');
|
| 7 |
+
this.elementName = document.getElementById('element-name');
|
| 8 |
+
this.elementDetails = document.getElementById('element-details');
|
| 9 |
+
this.spectrumContainer = document.getElementById('spectrum-container');
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
showSpectrum(symbol) {
|
| 13 |
+
const elementData = this.dataManager.getElementData(symbol);
|
| 14 |
+
|
| 15 |
+
if (!elementData) {
|
| 16 |
+
this.showNoData(symbol);
|
| 17 |
+
return;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Update element info
|
| 21 |
+
this.elementName.textContent = elementData.name;
|
| 22 |
+
this.elementDetails.textContent = `Symbol: ${elementData.symbol} | Atomic Number: ${elementData.atomicNumber}`;
|
| 23 |
+
|
| 24 |
+
// Create spectrum visualization
|
| 25 |
+
this.createSpectralLines(elementData.spectralLines);
|
| 26 |
+
|
| 27 |
+
// Show panel
|
| 28 |
+
this.infoPanel.classList.remove('hidden');
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
hideSpectrum() {
|
| 32 |
+
this.infoPanel.classList.add('hidden');
|
| 33 |
+
this.spectrumContainer.innerHTML = '';
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
createSpectralLines(lines) {
|
| 37 |
+
this.spectrumContainer.innerHTML = '';
|
| 38 |
+
|
| 39 |
+
if (!lines || lines.length === 0) {
|
| 40 |
+
this.spectrumContainer.innerHTML = '<p>No spectral data available</p>';
|
| 41 |
+
return;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Sort by intensity and limit to top 20
|
| 45 |
+
const sortedLines = [...lines].sort((a, b) => (b.intensity || 0) - (a.intensity || 0));
|
| 46 |
+
const displayLines = sortedLines.slice(0, 20);
|
| 47 |
+
|
| 48 |
+
// Create canvas for spectrum
|
| 49 |
+
const canvas = document.createElement('canvas');
|
| 50 |
+
canvas.width = 360;
|
| 51 |
+
canvas.height = 200;
|
| 52 |
+
canvas.style.width = '100%';
|
| 53 |
+
canvas.style.border = '1px solid rgba(255, 255, 255, 0.2)';
|
| 54 |
+
canvas.style.borderRadius = '5px';
|
| 55 |
+
|
| 56 |
+
const ctx = canvas.getContext('2d');
|
| 57 |
+
|
| 58 |
+
// Draw background
|
| 59 |
+
ctx.fillStyle = '#000';
|
| 60 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 61 |
+
|
| 62 |
+
// Find wavelength range
|
| 63 |
+
const wavelengths = displayLines.map(l => l.wavelength);
|
| 64 |
+
const minWave = Math.min(...wavelengths, 380);
|
| 65 |
+
const maxWave = Math.max(...wavelengths, 750);
|
| 66 |
+
const range = maxWave - minWave;
|
| 67 |
+
|
| 68 |
+
// Draw wavelength scale
|
| 69 |
+
ctx.fillStyle = '#666';
|
| 70 |
+
ctx.font = '10px Arial';
|
| 71 |
+
ctx.textAlign = 'center';
|
| 72 |
+
for (let w = Math.ceil(minWave / 50) * 50; w <= maxWave; w += 50) {
|
| 73 |
+
const x = ((w - minWave) / range) * (canvas.width - 40) + 20;
|
| 74 |
+
ctx.fillRect(x, canvas.height - 20, 1, 10);
|
| 75 |
+
ctx.fillText(w + 'nm', x, canvas.height - 5);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Draw spectral lines
|
| 79 |
+
displayLines.forEach(line => {
|
| 80 |
+
const { color, opacity } = getSpectrumColor(line.wavelength);
|
| 81 |
+
const x = ((line.wavelength - minWave) / range) * (canvas.width - 40) + 20;
|
| 82 |
+
const height = (line.intensity || 0.5) * 140;
|
| 83 |
+
|
| 84 |
+
ctx.fillStyle = `rgba(${Math.floor(color.r * 255)}, ${Math.floor(color.g * 255)}, ${Math.floor(color.b * 255)}, ${opacity})`;
|
| 85 |
+
ctx.fillRect(x - 2, canvas.height - 30 - height, 4, height);
|
| 86 |
+
|
| 87 |
+
// Draw wavelength label for prominent lines
|
| 88 |
+
if (line.intensity > 0.5) {
|
| 89 |
+
ctx.fillStyle = '#fff';
|
| 90 |
+
ctx.font = '9px Arial';
|
| 91 |
+
ctx.save();
|
| 92 |
+
ctx.translate(x, canvas.height - 35 - height);
|
| 93 |
+
ctx.rotate(-Math.PI / 2);
|
| 94 |
+
ctx.fillText(line.wavelength.toFixed(1), 0, 0);
|
| 95 |
+
ctx.restore();
|
| 96 |
+
}
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
this.spectrumContainer.appendChild(canvas);
|
| 100 |
+
|
| 101 |
+
// Add line details
|
| 102 |
+
const detailsDiv = document.createElement('div');
|
| 103 |
+
detailsDiv.style.marginTop = '10px';
|
| 104 |
+
detailsDiv.style.fontSize = '12px';
|
| 105 |
+
detailsDiv.style.maxHeight = '200px';
|
| 106 |
+
detailsDiv.style.overflowY = 'auto';
|
| 107 |
+
|
| 108 |
+
detailsDiv.innerHTML = '<h3 style="margin-bottom: 10px;">Spectral Lines:</h3>';
|
| 109 |
+
|
| 110 |
+
displayLines.forEach(line => {
|
| 111 |
+
const lineDiv = document.createElement('div');
|
| 112 |
+
lineDiv.style.marginBottom = '5px';
|
| 113 |
+
lineDiv.style.padding = '5px';
|
| 114 |
+
lineDiv.style.background = 'rgba(255, 255, 255, 0.05)';
|
| 115 |
+
lineDiv.style.borderRadius = '3px';
|
| 116 |
+
|
| 117 |
+
const { color } = getSpectrumColor(line.wavelength);
|
| 118 |
+
const colorBox = `<span style="display: inline-block; width: 12px; height: 12px; background: rgb(${Math.floor(color.r * 255)}, ${Math.floor(color.g * 255)}, ${Math.floor(color.b * 255)}); border: 1px solid #fff; margin-right: 5px;"></span>`;
|
| 119 |
+
|
| 120 |
+
lineDiv.innerHTML = `${colorBox}${line.wavelength.toFixed(1)} nm (Intensity: ${(line.intensity || 0).toFixed(2)})${line.transition ? ' - ' + line.transition : ''}`;
|
| 121 |
+
detailsDiv.appendChild(lineDiv);
|
| 122 |
+
});
|
| 123 |
+
|
| 124 |
+
this.spectrumContainer.appendChild(detailsDiv);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
showNoData(symbol) {
|
| 128 |
+
this.elementName.textContent = symbol;
|
| 129 |
+
this.elementDetails.textContent = 'Element information';
|
| 130 |
+
this.spectrumContainer.innerHTML = '<p>Spectral data not available for this element</p>';
|
| 131 |
+
this.infoPanel.classList.remove('hidden');
|
| 132 |
+
}
|
| 133 |
+
}
|
all-elements-data.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Complete periodic table data with cosmic origins and spectral lines
|
| 2 |
+
// This will be integrated into index.html
|
| 3 |
+
|
| 4 |
+
const ALL_ELEMENTS = [
|
| 5 |
+
// Period 1
|
| 6 |
+
{ symbol: "H", name: "Hydrogen", atomicNumber: 1, category: "nonmetal", row: 1, col: 1,
|
| 7 |
+
cosmicOrigin: "Big Bang", originColor: "#FFD700",
|
| 8 |
+
astronomyInfo: "The most abundant element in the universe, created during the Big Bang 13.8 billion years ago. Hydrogen makes up about 75% of all normal matter and is the fuel that powers stars through nuclear fusion.",
|
| 9 |
+
spectralLines: [{ wavelength: 656.3, intensity: 1.0 }, { wavelength: 486.1, intensity: 0.5 }, { wavelength: 434.0, intensity: 0.3 }, { wavelength: 410.2, intensity: 0.2 }]},
|
| 10 |
+
|
| 11 |
+
{ symbol: "He", name: "Helium", atomicNumber: 2, category: "noble-gas", row: 1, col: 18,
|
| 12 |
+
cosmicOrigin: "Big Bang", originColor: "#FFD700",
|
| 13 |
+
astronomyInfo: "The second most abundant element in the universe, primarily created during the Big Bang. Also produced by nuclear fusion in stars.",
|
| 14 |
+
spectralLines: [{ wavelength: 587.6, intensity: 1.0 }, { wavelength: 667.8, intensity: 0.3 }, { wavelength: 501.6, intensity: 0.4 }]},
|
| 15 |
+
|
| 16 |
+
// Period 2
|
| 17 |
+
{ symbol: "Li", name: "Lithium", atomicNumber: 3, category: "alkali-metal", row: 2, col: 1,
|
| 18 |
+
cosmicOrigin: "Cosmic Ray & Dying Stars", originColor: "#9370DB",
|
| 19 |
+
astronomyInfo: "Created through multiple processes: some from the Big Bang, most from dying low-mass stars, and some isotopes from cosmic ray collisions.",
|
| 20 |
+
spectralLines: [{ wavelength: 670.8, intensity: 1.0 }, { wavelength: 610.4, intensity: 0.3 }]},
|
| 21 |
+
|
| 22 |
+
{ symbol: "Be", name: "Beryllium", atomicNumber: 4, category: "alkaline-earth", row: 2, col: 2,
|
| 23 |
+
cosmicOrigin: "Cosmic Ray Fission", originColor: "#9370DB",
|
| 24 |
+
astronomyInfo: "Created by cosmic ray spallation - when high-energy cosmic rays collide with heavier elements in space, breaking them apart.",
|
| 25 |
+
spectralLines: [{ wavelength: 234.9, intensity: 1.0 }, { wavelength: 313.0, intensity: 0.4 }]},
|
element-science-data.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Chemistry and Physics data for all elements to be added:
|
| 2 |
+
|
| 3 |
+
H: chemistryInfo: "The simplest element with one proton and one electron. Forms covalent bonds in H₂ molecules. Essential for water (H₂O), acids, and all organic compounds. Has three isotopes: protium, deuterium, and tritium."
|
| 4 |
+
physicsInfo: "Atomic mass: 1.008 u. Ionization energy: 13.6 eV. The only atom with exact analytical solution to Schrödinger equation. Melting point: -259°C, Boiling point: -253°C."
|
| 5 |
+
|
| 6 |
+
He: chemistryInfo: "Noble gas with complete electron shell, chemically inert. Does not form compounds under normal conditions. Lowest boiling point (-269°C). Used in balloons, diving, and cryogenics."
|
| 7 |
+
physicsInfo: "Atomic mass: 4.003 u. Ionization energy: 24.6 eV (highest). Exhibits superfluidity below 2.17 K. Two stable isotopes: ³He and ⁴He."
|
| 8 |
+
|
| 9 |
+
Li: chemistryInfo: "Highly reactive alkali metal, stored in oil. Forms ionic compounds with halogens. Lightest metal and least dense solid. Used in rechargeable batteries and psychiatric medication."
|
| 10 |
+
physicsInfo: "Atomic mass: 6.94 u. Melting point: 180.5°C. Highest specific heat of any solid. Two stable isotopes: ⁶Li and ⁷Li. Excellent conductor."
|
| 11 |
+
|
| 12 |
+
Be: chemistryInfo: "Alkaline earth metal, forms strong covalent bonds. Toxic and carcinogenic. Used in aerospace alloys and X-ray windows. Forms Be²⁺ ions."
|
| 13 |
+
physicsInfo: "Atomic mass: 9.012 u. Melting point: 1287°C. High stiffness-to-weight ratio. Transparent to X-rays. One stable isotope: ⁹Be."
|
| 14 |
+
|
| 15 |
+
C: chemistryInfo: "Basis of organic chemistry, forms 4 covalent bonds. Exists as diamond, graphite, and fullerenes. Essential for all known life. Forms millions of compounds."
|
| 16 |
+
physicsInfo: "Atomic mass: 12.011 u. Sublimes at 3642°C. Diamond is hardest natural material. Graphene has highest strength. Isotopes: ¹²C, ¹³C, ¹⁴C (radioactive)."
|
| 17 |
+
|
| 18 |
+
N: chemistryInfo: "Diatomic gas (N₂) with triple bond, very stable. Essential for proteins and DNA. Forms nitrates and ammonia. Makes up 78% of Earth's atmosphere."
|
| 19 |
+
physicsInfo: "Atomic mass: 14.007 u. Boiling point: -196°C. Triple bond energy: 945 kJ/mol. Two stable isotopes: ¹⁴N, ¹⁵N."
|
| 20 |
+
|
| 21 |
+
O: chemistryInfo: "Highly reactive, forms oxides with most elements. Essential for respiration. Exists as O₂ and O₃ (ozone). Second most electronegative element."
|
| 22 |
+
physicsInfo: "Atomic mass: 15.999 u. Boiling point: -183°C. Paramagnetic in liquid form. Three stable isotopes: ¹⁶O, ¹⁷O, ¹⁸O."
|
| 23 |
+
|
| 24 |
+
F: chemistryInfo: "Most electronegative and reactive element. Forms fluorides with all elements except He, Ne, Ar. Used in toothpaste and Teflon. Highly toxic."
|
| 25 |
+
physicsInfo: "Atomic mass: 18.998 u. Boiling point: -188°C. Electronegativity: 4.0 (highest). One stable isotope: ¹⁹F."
|
| 26 |
+
|
| 27 |
+
Ne: chemistryInfo: "Noble gas, chemically inert. No known compounds. Used in neon signs (red-orange glow), lasers, and cryogenic refrigeration."
|
| 28 |
+
physicsInfo: "Atomic mass: 20.180 u. Boiling point: -246°C. Three stable isotopes: ²⁰Ne, ²¹Ne, ²²Ne. Ionization energy: 21.6 eV."
|
| 29 |
+
|
| 30 |
+
Na: chemistryInfo: "Highly reactive alkali metal, reacts violently with water. Forms Na⁺ ions. Essential for nerve function. Found in table salt (NaCl)."
|
| 31 |
+
physicsInfo: "Atomic mass: 22.990 u. Melting point: 98°C. Soft enough to cut with knife. One stable isotope: ²³Na. Excellent conductor."
|
elementLayout.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const ELEMENT_LAYOUT = {
|
| 2 |
+
tileSize: 1.0,
|
| 3 |
+
spacing: 0.1,
|
| 4 |
+
|
| 5 |
+
// Simplified layout for the 10 elements we have data for
|
| 6 |
+
positions: {
|
| 7 |
+
"H": { row: 1, column: 1 },
|
| 8 |
+
"He": { row: 1, column: 18 },
|
| 9 |
+
"Li": { row: 2, column: 1 },
|
| 10 |
+
"C": { row: 2, column: 14 },
|
| 11 |
+
"N": { row: 2, column: 15 },
|
| 12 |
+
"O": { row: 2, column: 16 },
|
| 13 |
+
"Na": { row: 3, column: 1 },
|
| 14 |
+
"Fe": { row: 4, column: 8 },
|
| 15 |
+
"Cu": { row: 4, column: 11 },
|
| 16 |
+
"Au": { row: 6, column: 11 }
|
| 17 |
+
}
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export const ELEMENT_COLORS = {
|
| 21 |
+
"metal": 0x4A90E2,
|
| 22 |
+
"nonmetal": 0x7ED321,
|
| 23 |
+
"metalloid": 0xF5A623,
|
| 24 |
+
"noble-gas": 0xBD10E0,
|
| 25 |
+
"alkali-metal": 0xFF6B6B,
|
| 26 |
+
"alkaline-earth": 0xFFA07A,
|
| 27 |
+
"transition-metal": 0x87CEEB,
|
| 28 |
+
"lanthanide": 0xFFD700,
|
| 29 |
+
"actinide": 0xFF69B4,
|
| 30 |
+
"halogen": 0x50E3C2,
|
| 31 |
+
"unknown": 0xCCCCCC
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
export function getElementPosition(symbol) {
|
| 35 |
+
const pos = ELEMENT_LAYOUT.positions[symbol];
|
| 36 |
+
if (!pos) return null;
|
| 37 |
+
|
| 38 |
+
const tileSize = ELEMENT_LAYOUT.tileSize;
|
| 39 |
+
const spacing = ELEMENT_LAYOUT.spacing;
|
| 40 |
+
const unit = tileSize + spacing;
|
| 41 |
+
|
| 42 |
+
// Center the periodic table
|
| 43 |
+
const x = (pos.column - 9.5) * unit;
|
| 44 |
+
const y = -(pos.row - 3.5) * unit;
|
| 45 |
+
const z = 0;
|
| 46 |
+
|
| 47 |
+
return { x, y, z, row: pos.row, column: pos.column };
|
| 48 |
+
}
|
index.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
main.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { SceneManager } from './SceneManager.js';
|
| 2 |
+
import { DataManager } from './DataManager.js';
|
| 3 |
+
import { PeriodicTableComponent } from './PeriodicTableComponent.js';
|
| 4 |
+
import { SpectrumDisplayComponent } from './SpectrumDisplayComponent.js';
|
| 5 |
+
import { InteractionController } from './InteractionController.js';
|
| 6 |
+
|
| 7 |
+
async function init() {
|
| 8 |
+
const loadingDiv = document.getElementById('loading');
|
| 9 |
+
const appContainer = document.getElementById('app-container');
|
| 10 |
+
|
| 11 |
+
try {
|
| 12 |
+
// Initialize DataManager and load spectral data
|
| 13 |
+
const dataManager = new DataManager();
|
| 14 |
+
await dataManager.loadSpectralData('spectral-data.json');
|
| 15 |
+
|
| 16 |
+
// Initialize SceneManager
|
| 17 |
+
const container = document.getElementById('canvas-container');
|
| 18 |
+
const sceneManager = new SceneManager(container);
|
| 19 |
+
|
| 20 |
+
if (!sceneManager.initialize()) {
|
| 21 |
+
throw new Error('Failed to initialize WebGL');
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Create PeriodicTableComponent
|
| 25 |
+
const periodicTable = new PeriodicTableComponent(
|
| 26 |
+
sceneManager.getScene(),
|
| 27 |
+
dataManager
|
| 28 |
+
);
|
| 29 |
+
periodicTable.createPeriodicTable();
|
| 30 |
+
|
| 31 |
+
// Create SpectrumDisplayComponent
|
| 32 |
+
const spectrumDisplay = new SpectrumDisplayComponent(dataManager);
|
| 33 |
+
|
| 34 |
+
// Create InteractionController
|
| 35 |
+
const interactionController = new InteractionController(
|
| 36 |
+
sceneManager,
|
| 37 |
+
periodicTable,
|
| 38 |
+
spectrumDisplay
|
| 39 |
+
);
|
| 40 |
+
interactionController.initialize();
|
| 41 |
+
|
| 42 |
+
// Start animation loop
|
| 43 |
+
sceneManager.start();
|
| 44 |
+
|
| 45 |
+
// Hide loading indicator
|
| 46 |
+
loadingDiv.style.display = 'none';
|
| 47 |
+
appContainer.style.display = 'block';
|
| 48 |
+
|
| 49 |
+
} catch (error) {
|
| 50 |
+
console.error('Initialization error:', error);
|
| 51 |
+
loadingDiv.innerHTML = `
|
| 52 |
+
<div style="color: #ff6b6b;">
|
| 53 |
+
<h2>Failed to Load Application</h2>
|
| 54 |
+
<p>${error.message}</p>
|
| 55 |
+
<p>Please refresh the page to try again.</p>
|
| 56 |
+
</div>
|
| 57 |
+
`;
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Start the application when DOM is ready
|
| 62 |
+
if (document.readyState === 'loading') {
|
| 63 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 64 |
+
} else {
|
| 65 |
+
init();
|
| 66 |
+
}
|
spectral-data.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"elements": [
|
| 3 |
+
{
|
| 4 |
+
"symbol": "H",
|
| 5 |
+
"name": "Hydrogen",
|
| 6 |
+
"atomicNumber": 1,
|
| 7 |
+
"category": "nonmetal",
|
| 8 |
+
"spectralLines": [
|
| 9 |
+
{ "wavelength": 656.3, "intensity": 1.0, "transition": "3→2 (Hα)" },
|
| 10 |
+
{ "wavelength": 486.1, "intensity": 0.5, "transition": "4→2 (Hβ)" },
|
| 11 |
+
{ "wavelength": 434.0, "intensity": 0.3, "transition": "5→2 (Hγ)" },
|
| 12 |
+
{ "wavelength": 410.2, "intensity": 0.2, "transition": "6→2 (Hδ)" }
|
| 13 |
+
]
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"symbol": "He",
|
| 17 |
+
"name": "Helium",
|
| 18 |
+
"atomicNumber": 2,
|
| 19 |
+
"category": "noble-gas",
|
| 20 |
+
"spectralLines": [
|
| 21 |
+
{ "wavelength": 587.6, "intensity": 1.0 },
|
| 22 |
+
{ "wavelength": 667.8, "intensity": 0.3 },
|
| 23 |
+
{ "wavelength": 501.6, "intensity": 0.4 },
|
| 24 |
+
{ "wavelength": 447.1, "intensity": 0.2 }
|
| 25 |
+
]
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"symbol": "Li",
|
| 29 |
+
"name": "Lithium",
|
| 30 |
+
"atomicNumber": 3,
|
| 31 |
+
"category": "alkali-metal",
|
| 32 |
+
"spectralLines": [
|
| 33 |
+
{ "wavelength": 670.8, "intensity": 1.0 },
|
| 34 |
+
{ "wavelength": 610.4, "intensity": 0.3 }
|
| 35 |
+
]
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"symbol": "C",
|
| 39 |
+
"name": "Carbon",
|
| 40 |
+
"atomicNumber": 6,
|
| 41 |
+
"category": "nonmetal",
|
| 42 |
+
"spectralLines": [
|
| 43 |
+
{ "wavelength": 247.9, "intensity": 1.0 },
|
| 44 |
+
{ "wavelength": 426.7, "intensity": 0.4 },
|
| 45 |
+
{ "wavelength": 538.0, "intensity": 0.3 }
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"symbol": "N",
|
| 50 |
+
"name": "Nitrogen",
|
| 51 |
+
"atomicNumber": 7,
|
| 52 |
+
"category": "nonmetal",
|
| 53 |
+
"spectralLines": [
|
| 54 |
+
{ "wavelength": 399.5, "intensity": 0.8 },
|
| 55 |
+
{ "wavelength": 500.5, "intensity": 0.5 },
|
| 56 |
+
{ "wavelength": 567.6, "intensity": 0.3 }
|
| 57 |
+
]
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"symbol": "O",
|
| 61 |
+
"name": "Oxygen",
|
| 62 |
+
"atomicNumber": 8,
|
| 63 |
+
"category": "nonmetal",
|
| 64 |
+
"spectralLines": [
|
| 65 |
+
{ "wavelength": 777.4, "intensity": 1.0 },
|
| 66 |
+
{ "wavelength": 844.6, "intensity": 0.7 },
|
| 67 |
+
{ "wavelength": 615.8, "intensity": 0.4 }
|
| 68 |
+
]
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"symbol": "Na",
|
| 72 |
+
"name": "Sodium",
|
| 73 |
+
"atomicNumber": 11,
|
| 74 |
+
"category": "alkali-metal",
|
| 75 |
+
"spectralLines": [
|
| 76 |
+
{ "wavelength": 589.0, "intensity": 1.0 },
|
| 77 |
+
{ "wavelength": 589.6, "intensity": 0.9 },
|
| 78 |
+
{ "wavelength": 568.3, "intensity": 0.2 }
|
| 79 |
+
]
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"symbol": "Fe",
|
| 83 |
+
"name": "Iron",
|
| 84 |
+
"atomicNumber": 26,
|
| 85 |
+
"category": "transition-metal",
|
| 86 |
+
"spectralLines": [
|
| 87 |
+
{ "wavelength": 438.4, "intensity": 0.9 },
|
| 88 |
+
{ "wavelength": 440.5, "intensity": 0.8 },
|
| 89 |
+
{ "wavelength": 495.8, "intensity": 0.6 },
|
| 90 |
+
{ "wavelength": 526.9, "intensity": 0.7 },
|
| 91 |
+
{ "wavelength": 532.8, "intensity": 0.5 }
|
| 92 |
+
]
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"symbol": "Cu",
|
| 96 |
+
"name": "Copper",
|
| 97 |
+
"atomicNumber": 29,
|
| 98 |
+
"category": "transition-metal",
|
| 99 |
+
"spectralLines": [
|
| 100 |
+
{ "wavelength": 324.8, "intensity": 1.0 },
|
| 101 |
+
{ "wavelength": 327.4, "intensity": 0.9 },
|
| 102 |
+
{ "wavelength": 510.6, "intensity": 0.4 },
|
| 103 |
+
{ "wavelength": 521.8, "intensity": 0.5 }
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"symbol": "Au",
|
| 108 |
+
"name": "Gold",
|
| 109 |
+
"atomicNumber": 79,
|
| 110 |
+
"category": "transition-metal",
|
| 111 |
+
"spectralLines": [
|
| 112 |
+
{ "wavelength": 267.6, "intensity": 1.0 },
|
| 113 |
+
{ "wavelength": 312.3, "intensity": 0.7 },
|
| 114 |
+
{ "wavelength": 479.3, "intensity": 0.3 }
|
| 115 |
+
]
|
| 116 |
+
}
|
| 117 |
+
]
|
| 118 |
+
}
|
styles.css
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
margin: 0;
|
| 3 |
+
padding: 0;
|
| 4 |
+
box-sizing: border-box;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
body {
|
| 8 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 9 |
+
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
| 10 |
+
color: #fff;
|
| 11 |
+
overflow: hidden;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
#loading {
|
| 15 |
+
position: fixed;
|
| 16 |
+
top: 0;
|
| 17 |
+
left: 0;
|
| 18 |
+
width: 100%;
|
| 19 |
+
height: 100%;
|
| 20 |
+
background: rgba(0, 0, 0, 0.9);
|
| 21 |
+
display: flex;
|
| 22 |
+
flex-direction: column;
|
| 23 |
+
justify-content: center;
|
| 24 |
+
align-items: center;
|
| 25 |
+
z-index: 1000;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.spinner {
|
| 29 |
+
width: 50px;
|
| 30 |
+
height: 50px;
|
| 31 |
+
border: 5px solid rgba(255, 255, 255, 0.3);
|
| 32 |
+
border-top-color: #fff;
|
| 33 |
+
border-radius: 50%;
|
| 34 |
+
animation: spin 1s linear infinite;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
@keyframes spin {
|
| 38 |
+
to { transform: rotate(360deg); }
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
#loading p {
|
| 42 |
+
margin-top: 20px;
|
| 43 |
+
font-size: 18px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
#app-container {
|
| 47 |
+
width: 100vw;
|
| 48 |
+
height: 100vh;
|
| 49 |
+
position: relative;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
#canvas-container {
|
| 53 |
+
width: 100%;
|
| 54 |
+
height: 100%;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#info-panel {
|
| 58 |
+
position: absolute;
|
| 59 |
+
top: 20px;
|
| 60 |
+
right: 20px;
|
| 61 |
+
background: rgba(0, 0, 0, 0.8);
|
| 62 |
+
padding: 20px;
|
| 63 |
+
border-radius: 10px;
|
| 64 |
+
max-width: 400px;
|
| 65 |
+
max-height: 80vh;
|
| 66 |
+
overflow-y: auto;
|
| 67 |
+
backdrop-filter: blur(10px);
|
| 68 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
#info-panel.hidden {
|
| 72 |
+
display: none;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
#info-panel h2 {
|
| 76 |
+
margin-bottom: 10px;
|
| 77 |
+
color: #4fc3f7;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
#element-details {
|
| 81 |
+
margin-bottom: 20px;
|
| 82 |
+
font-size: 14px;
|
| 83 |
+
color: #ccc;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
#spectrum-container {
|
| 87 |
+
margin-top: 20px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
#controls-info {
|
| 91 |
+
position: absolute;
|
| 92 |
+
bottom: 60px;
|
| 93 |
+
left: 50%;
|
| 94 |
+
transform: translateX(-50%);
|
| 95 |
+
background: rgba(0, 0, 0, 0.7);
|
| 96 |
+
padding: 10px 20px;
|
| 97 |
+
border-radius: 5px;
|
| 98 |
+
font-size: 14px;
|
| 99 |
+
text-align: center;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
footer {
|
| 103 |
+
position: absolute;
|
| 104 |
+
bottom: 10px;
|
| 105 |
+
left: 50%;
|
| 106 |
+
transform: translateX(-50%);
|
| 107 |
+
font-size: 12px;
|
| 108 |
+
text-align: center;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
footer a {
|
| 112 |
+
color: #4fc3f7;
|
| 113 |
+
text-decoration: none;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
footer a:hover {
|
| 117 |
+
text-decoration: underline;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.hidden {
|
| 121 |
+
display: none;
|
| 122 |
+
}
|
wavelengthUtils.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'three';
|
| 2 |
+
|
| 3 |
+
export function wavelengthToRGB(wavelength) {
|
| 4 |
+
let r, g, b;
|
| 5 |
+
|
| 6 |
+
if (wavelength >= 380 && wavelength < 440) {
|
| 7 |
+
r = -(wavelength - 440) / (440 - 380);
|
| 8 |
+
g = 0.0;
|
| 9 |
+
b = 1.0;
|
| 10 |
+
} else if (wavelength >= 440 && wavelength < 490) {
|
| 11 |
+
r = 0.0;
|
| 12 |
+
g = (wavelength - 440) / (490 - 440);
|
| 13 |
+
b = 1.0;
|
| 14 |
+
} else if (wavelength >= 490 && wavelength < 510) {
|
| 15 |
+
r = 0.0;
|
| 16 |
+
g = 1.0;
|
| 17 |
+
b = -(wavelength - 510) / (510 - 490);
|
| 18 |
+
} else if (wavelength >= 510 && wavelength < 580) {
|
| 19 |
+
r = (wavelength - 510) / (580 - 510);
|
| 20 |
+
g = 1.0;
|
| 21 |
+
b = 0.0;
|
| 22 |
+
} else if (wavelength >= 580 && wavelength < 645) {
|
| 23 |
+
r = 1.0;
|
| 24 |
+
g = -(wavelength - 645) / (645 - 580);
|
| 25 |
+
b = 0.0;
|
| 26 |
+
} else if (wavelength >= 645 && wavelength <= 750) {
|
| 27 |
+
r = 1.0;
|
| 28 |
+
g = 0.0;
|
| 29 |
+
b = 0.0;
|
| 30 |
+
} else {
|
| 31 |
+
r = 0.0;
|
| 32 |
+
g = 0.0;
|
| 33 |
+
b = 0.0;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Apply intensity correction for edge wavelengths
|
| 37 |
+
let factor;
|
| 38 |
+
if (wavelength >= 380 && wavelength < 420) {
|
| 39 |
+
factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380);
|
| 40 |
+
} else if (wavelength >= 420 && wavelength <= 700) {
|
| 41 |
+
factor = 1.0;
|
| 42 |
+
} else if (wavelength > 700 && wavelength <= 750) {
|
| 43 |
+
factor = 0.3 + 0.7 * (750 - wavelength) / (750 - 700);
|
| 44 |
+
} else {
|
| 45 |
+
factor = 0.0;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Apply gamma correction
|
| 49 |
+
const gamma = 0.80;
|
| 50 |
+
r = Math.pow(r * factor, gamma);
|
| 51 |
+
g = Math.pow(g * factor, gamma);
|
| 52 |
+
b = Math.pow(b * factor, gamma);
|
| 53 |
+
|
| 54 |
+
return new THREE.Color(r, g, b);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export function getSpectrumColor(wavelength) {
|
| 58 |
+
if (wavelength < 380) {
|
| 59 |
+
// UV - use violet with reduced opacity
|
| 60 |
+
return { color: new THREE.Color(0x9400D3), opacity: 0.5 };
|
| 61 |
+
} else if (wavelength > 750) {
|
| 62 |
+
// IR - use dark red with reduced opacity
|
| 63 |
+
return { color: new THREE.Color(0x8B0000), opacity: 0.5 };
|
| 64 |
+
} else {
|
| 65 |
+
// Visible spectrum
|
| 66 |
+
return { color: wavelengthToRGB(wavelength), opacity: 1.0 };
|
| 67 |
+
}
|
| 68 |
+
}
|