muthuk1's picture
Convert OpenMAIC from Next.js to React (Vite)
f56a29b verified
# 3D Visualization Content Generator
Generate a self-contained HTML 3D visualization with embedded widget configuration using Three.js.
## Output Structure
Your output must be a complete HTML document with:
1. **Standard HTML5 structure**
2. **Three.js loaded from CDN** (use unpkg or cdnjs)
3. **Embedded widget configuration** in a `<script type="application/json" id="widget-config">` tag
4. **3D scene with interactive controls** (OrbitControls, sliders, buttons, **ZOOM BUTTONS**)
5. **Mobile-responsive design**
6. **postMessage listener** for teacher actions (REQUIRED)
## ⚠️ CRITICAL REQUIREMENTS
### 1. LIGHTING - Objects MUST be clearly visible
**ALWAYS ensure:**
- Background should NOT be pure black (use deep blue `#0a0a1a` or dark gradient)
- Ambient light intensity at least `0.4` (not 0.1!)
- Main objects MUST have dedicated lights illuminating them
- For planets/Earth, use bright diffuse color (not dark!)
- Add hemisphere light for natural ambient fill
```javascript
// GOOD lighting setup
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// Hemisphere light for natural lighting
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
scene.add(hemiLight);
// Main directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(10, 20, 10);
scene.add(directionalLight);
```
### 2. ZOOM CONTROLS - REQUIRED for mobile users
**MUST include zoom buttons** in the control panel:
```html
<!-- Add these buttons to your controls -->
<div class="zoom-controls">
<button id="zoom-in-btn" title="放大">+</button>
<button id="zoom-out-btn" title="缩小">−</button>
</div>
```
```javascript
// Zoom functionality
document.getElementById('zoom-in-btn').addEventListener('click', () => {
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
camera.position.addScaledVector(direction, 5);
});
document.getElementById('zoom-out-btn').addEventListener('click', () => {
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
camera.position.addScaledVector(direction, -5);
});
```
### 3. REALISTIC OBJECTS - Use procedural textures
**For Earth/planets, create realistic appearance:**
```javascript
// Create procedural Earth texture with continents
function createEarthTexture() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 256;
const ctx = canvas.getContext('2d');
// Ocean base (bright blue, not dark!)
ctx.fillStyle = '#1e90ff';
ctx.fillRect(0, 0, 512, 256);
// Add continents (green land masses)
ctx.fillStyle = '#228b22';
// Simple continent shapes (approximate)
// North America
ctx.beginPath();
ctx.ellipse(100, 80, 60, 40, 0, 0, Math.PI * 2);
ctx.fill();
// South America
ctx.beginPath();
ctx.ellipse(130, 160, 30, 50, 0.3, 0, Math.PI * 2);
ctx.fill();
// Europe/Africa
ctx.beginPath();
ctx.ellipse(270, 100, 40, 70, 0, 0, Math.PI * 2);
ctx.fill();
// Asia
ctx.beginPath();
ctx.ellipse(380, 70, 80, 50, 0, 0, Math.PI * 2);
ctx.fill();
// Australia
ctx.beginPath();
ctx.ellipse(420, 170, 30, 20, 0, 0, Math.PI * 2);
ctx.fill();
// Add ice caps
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 512, 15);
ctx.fillRect(0, 241, 512, 15);
// Add clouds (light patches)
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
for (let i = 0; i < 20; i++) {
const x = Math.random() * 512;
const y = Math.random() * 256;
ctx.beginPath();
ctx.ellipse(x, y, 30 + Math.random() * 20, 10 + Math.random() * 10, 0, 0, Math.PI * 2);
ctx.fill();
}
return new THREE.CanvasTexture(canvas);
}
// Create Earth with procedural texture
const earthGeometry = new THREE.SphereGeometry(1, 64, 64);
const earthMaterial = new THREE.MeshPhongMaterial({
map: createEarthTexture(),
specular: 0x333333,
shininess: 15
});
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
```
**For other planets:**
- **Mars**: Red-orange with dark patches (`#cd5c5c` base, `#8b4513` patches)
- **Jupiter**: Orange bands with white ovals
- **Sun**: Bright yellow-orange with glow effect (use emissive material)
- **Moon**: Gray with craters (use noise pattern)
## Widget Config Schema
```json
{
"type": "visualization3d",
"visualizationType": "solar",
"description": "Interactive solar system model",
"objects": [
{ "id": "sun", "type": "sphere", "material": { "type": "emissive", "color": "#FDB813" } },
{ "id": "earth", "type": "sphere", "material": { "type": "textured", "textureType": "earth" } }
],
"interactions": [
{ "type": "orbit", "target": "camera" },
{ "type": "slider", "param": "speed", "min": 0, "max": 10, "default": 1 },
{ "type": "button", "action": "zoomIn", "label": "放大" },
{ "type": "button", "action": "zoomOut", "label": "缩小" }
],
"presets": [
{ "name": "View Earth", "state": { "cameraTarget": "earth" } }
]
}
```
## Three.js Setup Template (Complete with Safeguards)
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Visualization</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
/* CRITICAL: Set body background to match scene - fallback if Three.js fails */
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #0a0a1a; /* MUST match scene.background color! */
}
#canvas-container { width: 100%; height: 100%; position: relative; }
canvas { display: block; }
/* Loading overlay - shows while Three.js initializes */
#loading {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: #0a0a1a;
display: flex;
align-items: center;
justify-content: center;
color: #aaa;
font-size: 16px;
z-index: 1000;
}
#loading .spinner {
width: 40px; height: 40px;
border: 3px solid #333;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Control panel - mobile friendly */
#controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(20, 20, 30, 0.9);
backdrop-filter: blur(12px);
padding: 16px;
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
align-items: center;
border-top: 1px solid rgba(255,255,255,0.1);
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 100px;
}
label {
font-size: 11px;
color: #aaa;
text-transform: uppercase;
letter-spacing: 0.5px;
}
input[type="range"] {
width: 100%;
height: 6px;
-webkit-appearance: none;
background: #333;
border-radius: 3px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: #6366f1;
border-radius: 50%;
cursor: pointer;
}
button {
padding: 12px 20px;
border: none;
border-radius: 8px;
background: #333;
color: white;
cursor: pointer;
font-size: 14px;
min-width: 44px;
min-height: 44px;
transition: all 0.2s;
}
button:hover { background: #444; }
button:active { transform: scale(0.95); }
button.primary { background: #6366f1; }
button.primary:hover { background: #5558e8; }
/* Zoom buttons side by side */
.zoom-btns {
display: flex;
gap: 8px;
}
.zoom-btns button {
width: 44px;
height: 44px;
font-size: 24px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
/* Info panel */
#info {
position: absolute;
top: 20px;
left: 20px;
background: rgba(20, 20, 30, 0.85);
backdrop-filter: blur(8px);
padding: 16px;
border-radius: 12px;
max-width: 280px;
border: 1px solid rgba(255,255,255,0.1);
}
#info h2 {
font-size: 16px;
color: #fbbf24;
margin-bottom: 8px;
}
#info p {
font-size: 13px;
color: #ccc;
line-height: 1.5;
}
@media (max-width: 600px) {
#info { display: none; }
#controls { padding: 12px 8px 24px; }
}
</style>
</head>
<body>
<!-- Loading overlay - REQUIRED -->
<div id="loading">
<div style="text-align:center;">
<div class="spinner"></div>
Loading 3D Scene...
</div>
</div>
<div id="canvas-container"></div>
<div id="info">
<h2>Scene Title</h2>
<p>Description text here.</p>
</div>
<div id="controls">
<div class="control-group">
<label>Speed</label>
<input type="range" id="speed-slider" min="0" max="5" step="0.1" value="1">
</div>
<div class="zoom-btns">
<button id="zoom-in-btn" title="Zoom In">+</button>
<button id="zoom-out-btn" title="Zoom Out">−</button>
</div>
<button id="reset-btn" class="primary">Reset</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// WebGL support check - REQUIRED
function checkWebGL() {
try {
const canvas = document.createElement('canvas');
return !!(window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
} catch(e) {
return false;
}
}
// Scene initialization with error handling - REQUIRED
async function initScene() {
try {
// Check WebGL support
if (!checkWebGL()) {
throw new Error('WebGL not supported in this browser');
}
const container = document.getElementById('canvas-container');
// Validate container dimensions - REQUIRED
const width = container.clientWidth || window.innerWidth;
const height = container.clientHeight || window.innerHeight;
if (width === 0 || height === 0) {
throw new Error('Container has zero dimensions');
}
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a); // MUST match body background!
// Camera with validated dimensions
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000);
camera.position.set(0, 5, 15);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
// OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// GOOD lighting setup - objects must be visible!
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
scene.add(hemiLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(10, 20, 10);
scene.add(directionalLight);
// Objects storage for later reference
const objects = {};
// Animation state
let animationSpeed = 1;
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Update animations...
controls.update();
renderer.render(scene, camera);
}
animate();
// Zoom controls - REQUIRED for mobile
document.getElementById('zoom-in-btn').addEventListener('click', () => {
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
camera.position.addScaledVector(direction, 3);
});
document.getElementById('zoom-out-btn').addEventListener('click', () => {
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
camera.position.addScaledVector(direction, -3);
});
// Reset button
document.getElementById('reset-btn').addEventListener('click', () => {
camera.position.set(0, 5, 15);
controls.target.set(0, 0, 0);
});
// Handle resize
window.addEventListener('resize', () => {
const newWidth = container.clientWidth || window.innerWidth;
const newHeight = container.clientHeight || window.innerHeight;
camera.aspect = newWidth / newHeight;
camera.updateProjectionMatrix();
renderer.setSize(newWidth, newHeight);
});
// Hide loading overlay - scene is ready
document.getElementById('loading').style.display = 'none';
} catch (error) {
console.error('Scene initialization failed:', error);
// Show error message in loading overlay
document.getElementById('loading').innerHTML =
`<div style="text-align:center;color:#ff6b6b;">
<div style="font-size:24px;margin-bottom:16px;">⚠️</div>
Failed to load 3D scene<br>
<small style="color:#888;">${error.message}</small><br>
<button onclick="location.reload()" style="margin-top:16px;padding:8px 16px;background:#6366f1;color:white;border:none;border-radius:6px;cursor:pointer;">Retry</button>
</div>`;
}
}
// Initialize scene
initScene();
</script>
<script type="application/json" id="widget-config">
{
"type": "visualization3d",
"visualizationType": "custom",
"description": "3D visualization",
"objects": [],
"interactions": []
}
</script>
</body>
</html>
```
## Visualization Types
### 1. Solar System (`solar`)
- Sun with emissive glow effect
- Planets with **procedural textures** (Earth with continents, Mars red, etc.)
- Orbital paths visible
- Zoom controls for mobile
- Bright lighting so planets are visible
### 2. Molecular (`molecular`)
- Atoms as colored spheres with high contrast
- Bonds as cylinders
- Labels for atom types
- Good ambient lighting
### 3. Anatomy (`anatomy`)
- Organs with distinct colors
- Transparent layers
- Labels and descriptions
### 4. Geometry (`geometry`)
- 3D shapes with distinct colors
- Edge highlighting
- Measurement annotations
### 5. Physics (`physics`)
- Trajectories with visible paths
- Force arrows
- Clear contrast between objects
### 6. Custom (`custom`)
- Follow the same lighting and zoom requirements
## Design Requirements
### 1. Visibility & Contrast
- Background: Use `#0a0a1a` or dark gradient (NOT pure black)
- Objects: Use bright, distinct colors
- Ambient light: At least 0.5 intensity
- Add hemisphere light for natural fill
### 2. Mobile Responsiveness
- Touch-friendly controls (44px minimum)
- Zoom buttons always visible
- OrbitControls works with touch
- Control panel at bottom for thumb access
### 3. Performance
- Use `requestAnimationFrame`
- Limit geometry complexity
- Use 64 segments for spheres (not 128)
### 4. Textures
- Create procedural textures using Canvas API
- No external image dependencies
- Earth: Blue ocean + green continents + white ice caps
- Planets: Appropriate colors with variations
## JavaScript Coding Rules
### 1. Switch Statement Scope (CRITICAL - Causes SyntaxError)
**WRONG - Variables redeclared across cases:**
```javascript
// This causes: SyntaxError: Identifier 'elementId' has already been declared
switch (action) {
case 'HIGHLIGHT_ELEMENT':
const { elementId, highlight } = payload; // First const
// ...
break;
case 'ANNOTATE_ELEMENT':
const { elementId, text } = payload; // ERROR! elementId already declared
// ...
break;
}
```
**CORRECT - Wrap each case in braces to create block scope:**
```javascript
// Each case has its own block scope
switch (action) {
case 'HIGHLIGHT_ELEMENT': {
const { elementId, highlight } = payload;
// ...
break;
}
case 'ANNOTATE_ELEMENT': {
const { elementId, text } = payload; // OK - different block scope
// ...
break;
}
case 'SET_WIDGET_STATE': {
const { cameraPosition, scale } = payload;
// ...
break;
}
}
```
**Alternative - Use different variable names:**
```javascript
switch (action) {
case 'HIGHLIGHT_ELEMENT':
const highlightData = payload;
// Use highlightData.elementId
break;
case 'ANNOTATE_ELEMENT':
const annotateData = payload;
// Use annotateData.elementId
break;
}
```
### 2. Teacher Actions Listener Pattern
Always wrap switch cases in braces:
```javascript
window.addEventListener('message', (event) => {
const { action, payload } = event.data;
switch (action) {
case 'SET_WIDGET_STATE': {
if (payload.cameraPosition) camera.position.set(...payload.cameraPosition);
if (payload.scale !== undefined) {
objects.cellGroup.scale.setScalar(payload.scale);
}
break;
}
case 'HIGHLIGHT_ELEMENT': {
const { elementId, highlight } = payload;
if (objects[elementId]) {
objects[elementId].forEach(mesh => {
mesh.material.emissive.set(highlight ? 0xffff00 : 0x000000);
});
}
break;
}
case 'ANNOTATE_ELEMENT': {
const { elementId, text } = payload;
// Create annotation tooltip
break;
}
}
});
```
## Output Format
Return ONLY the HTML document, no markdown fences or explanations.
**CRITICAL: Output EXACTLY ONE HTML document.**
- Do NOT duplicate content
- Do NOT include multiple `<!DOCTYPE html>` tags
- The output must end with exactly one `</html>` tag