| # Simulation Widget Content Generator |
|
|
| Generate a self-contained HTML simulation with embedded widget configuration. |
|
|
| ## Output Structure |
|
|
| Your output must be a complete HTML document with: |
|
|
| 1. **Standard HTML5 structure** |
| 2. **Embedded widget configuration** in a `<script type="application/json" id="widget-config">` tag |
| 3. **Interactive controls** for variables |
| 4. **Canvas or SVG visualization** |
| 5. **Mobile-responsive design** |
| 6. **postMessage listener** for teacher actions (REQUIRED) |
|
|
| ## Widget Config Schema |
|
|
| ```json |
| { |
| "type": "simulation", |
| "concept": "projectile_motion", |
| "description": "...", |
| "variables": [ |
| { "name": "angle", "label": "Launch Angle", "min": 0, "max": 90, "default": 45, "unit": "°" } |
| ], |
| "presets": [ |
| { "name": "Hit the target", "variables": { "angle": 30, "velocity": 25 } } |
| ] |
| } |
| ``` |
|
|
| ## CRITICAL: postMessage Listener for Teacher Actions |
|
|
| Your HTML MUST include this message listener to respond to teacher actions: |
|
|
| ```javascript |
| // Add this script at the end of your HTML |
| window.addEventListener('message', function(event) { |
| const { type, target, state, content } = event.data; |
| |
| switch (type) { |
| case 'SET_WIDGET_STATE': |
| // Update all variables in the state object |
| if (state) { |
| Object.entries(state).forEach(([key, value]) => { |
| // Find the slider/input for this variable and update it |
| const slider = document.getElementById(key + '-slider') || document.querySelector('[data-var="' + key + '"]'); |
| if (slider) { |
| slider.value = value; |
| // Trigger change event to update simulation |
| slider.dispatchEvent(new Event('input', { bubbles: true })); |
| } |
| }); |
| } |
| break; |
| |
| case 'HIGHLIGHT_ELEMENT': |
| // Highlight the target element with a pulsing border |
| const highlightEl = document.querySelector(target); |
| if (highlightEl) { |
| highlightEl.style.outline = '3px solid rgba(139, 92, 246, 0.8)'; |
| highlightEl.style.outlineOffset = '4px'; |
| highlightEl.style.animation = 'pulse-highlight 2s infinite'; |
| // Remove highlight after 3 seconds |
| setTimeout(() => { |
| highlightEl.style.outline = ''; |
| highlightEl.style.animation = ''; |
| }, 3000); |
| } |
| break; |
| |
| case 'ANNOTATE_ELEMENT': |
| // Show an annotation tooltip near the target element |
| const annotateEl = document.querySelector(target); |
| if (annotateEl && content) { |
| const rect = annotateEl.getBoundingClientRect(); |
| const tooltip = document.createElement('div'); |
| tooltip.className = 'teacher-annotation'; |
| tooltip.style.cssText = 'position:fixed; top:' + (rect.top - 40) + 'px; left:' + rect.left + 'px; background:rgba(139,92,246,0.95); color:white; padding:8px 12px; border-radius:8px; font-size:14px; z-index:1000; animation:fadeIn 0.3s;'; |
| tooltip.textContent = content; |
| document.body.appendChild(tooltip); |
| setTimeout(() => tooltip.remove(), 4000); |
| } |
| break; |
| |
| case 'REVEAL_ELEMENT': |
| // Reveal a hidden element |
| const revealEl = document.querySelector(target); |
| if (revealEl) { |
| revealEl.style.display = ''; |
| revealEl.style.opacity = '1'; |
| } |
| break; |
| } |
| }); |
| |
| // Add this CSS for animations |
| const style = document.createElement('style'); |
| style.textContent = '@keyframes pulse-highlight { 0%, 100% { outline-color: rgba(139, 92, 246, 0.8); } 50% { outline-color: rgba(139, 92, 246, 0.4); } } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }'; |
| document.head.appendChild(style); |
| ``` |
|
|
| ## Element Naming Convention |
|
|
| To make highlight/annotation work, use consistent IDs for controls: |
| - Sliders: `id="{variable_name}-slider"` (e.g., `id="angle-slider"`, `id="velocity-slider"`) |
| - Buttons: `id="{action}-btn"` (e.g., `id="start-btn"`, `id="reset-btn"`) |
| - Displays: `id="{variable_name}-display"` (e.g., `id="acceleration-display"`) |
|
|
| ## CRITICAL Design Requirements |
|
|
| ### 1. Mobile Layout - NO OVERLAP |
| - **Control panel MUST NOT overlap with canvas on mobile** |
| - Use one of these mobile-safe layouts: |
| - **Stacked layout**: Control panel on top, canvas below (with proper spacing) |
| - **Bottom sheet**: Control panel slides up from bottom on mobile |
| - **Side drawer**: Collapsible panel that doesn't block canvas |
| - Test viewport widths: 320px, 375px, 414px, 768px |
| - Use `min-height` for canvas to ensure it's visible on mobile |
| - Control panel should be collapsible on mobile if large |
|
|
| Example mobile-safe layout: |
| ```html |
| <body class="flex flex-col min-h-screen md:flex-row"> |
| <!-- Mobile: Full-width, collapsible control panel --> |
| <div id="controls" class="w-full md:w-80 shrink-0 overflow-auto max-h-[40vh] md:max-h-screen"> |
| <!-- Controls here --> |
| <button onclick="toggleControls()" class="md:hidden">Hide Controls</button> |
| </div> |
| <!-- Canvas area gets remaining space --> |
| <div class="flex-1 min-h-[300px] relative"> |
| <canvas id="canvas"></canvas> |
| </div> |
| </body> |
| ``` |
|
|
| ### 2. Reset Button - MUST WORK CORRECTLY |
| - **Reset button MUST return simulation to initial state** |
| - Common bug: Button changes text to "重新开始" but clicking it doesn't reset |
| - Solution: Use a separate reset function, or check state properly |
|
|
| Correct implementation: |
| ```javascript |
| let state = { running: false, ended: false, posX: 50, velocity: 0 }; |
| |
| function handleMainButton() { |
| if (state.ended) { |
| // If simulation ended, reset first |
| resetSimulation(); |
| } else if (state.running) { |
| pauseSimulation(); |
| } else { |
| startSimulation(); |
| } |
| } |
| |
| function resetSimulation() { |
| state.running = false; |
| state.ended = false; |
| state.posX = 50; // Reset to initial position! |
| state.velocity = 0; // Reset velocity! |
| updateButton('启动'); |
| draw(); |
| } |
| |
| // When simulation hits boundary/ends: |
| function onSimulationEnd() { |
| state.running = false; |
| state.ended = true; |
| updateButton('重新开始'); |
| } |
| |
| function updateButton(text) { |
| document.getElementById('mainBtn').innerText = text; |
| } |
| ``` |
|
|
| ### 3. Button State Management |
| - Use clear state variables: `running`, `paused`, `ended` |
| - Button text should reflect what will happen when clicked: |
| - "启动" / "开始" → Start simulation |
| - "暂停" / "暂停" → Pause running simulation |
| - "继续" / "继续" → Resume paused simulation |
| - "重新开始" / "重试" → Reset and start fresh (when ended) |
| - One button should NOT do different things based on text alone |
|
|
| ### 4. Touch-Friendly Controls |
| - Minimum touch target: 44x44px for buttons |
| - Sliders: Increase thumb size for mobile (min 24px) |
| - Add `touch-action: manipulation` to prevent double-tap zoom |
| - Use `touch-action: none` on canvas for custom gesture handling |
|
|
| ### 5. Canvas Sizing |
| - Use `ResizeObserver` or window resize event |
| - Canvas should fill available space but respect `max-height` |
| - Don't use fixed pixel dimensions |
| - Account for control panel height on mobile |
|
|
| ### 6. Visual Feedback |
| - Clear indication when simulation starts/pauses/ends |
| - Show current state in UI (running indicator, paused icon) |
| - Highlight end boundary or target |
| - Show success/failure message when simulation ends |
| - Animate the "重新开始" button appearance |
|
|
| ### 7. Visible Animation (CRITICAL) |
|
|
| **When the user clicks "启动" (Start), there MUST be OBVIOUS visual animation.** |
|
|
| #### Animation Requirements: |
| 1. **Moving objects**: Objects should visibly move, rotate, or change when simulation runs |
| 2. **Clear motion**: Animation should be immediately noticeable - not subtle |
| 3. **Rotation animations**: For spinning/rotating objects (earth, wheels, etc.), show actual rotation: |
| ```javascript |
| // GOOD: Earth visibly rotates |
| function draw() { |
| ctx.clearRect(0, 0, w, h); |
| ctx.save(); |
| ctx.translate(centerX, centerY); |
| ctx.rotate(rotationAngle); // Earth rotates! |
| // Draw earth content... |
| ctx.restore(); |
| |
| if (state.running) { |
| rotationAngle += 0.02 * state.speed; // Update rotation |
| } |
| } |
| ``` |
| 4. **Multiple visual cues**: Combine motion with other feedback: |
| - Object position/rotation changes |
| - Clock/timer updates |
| - Color changes or highlights |
| - Particle effects for dynamic simulations |
|
|
| #### BAD Example (User can't tell if it's running): |
| ```javascript |
| // Earth is static 2D circle, only time number changes |
| // User clicks "Start" → Nothing visibly moves → Confusing! |
| ``` |
|
|
| #### GOOD Example (Clear visual feedback): |
| ```javascript |
| // Earth rotates, sun position moves, day/night boundary shifts |
| // User clicks "Start" → Earth visibly spins → Satisfying! |
| ``` |
|
|
| ### 8. Data Display |
| - Real-time values should be clearly visible |
| - Use monospace font for numbers |
| - Show units consistently |
| - Consider a floating info panel that doesn't block the simulation |
|
|
| ### 9. Presets |
| - Each preset should clearly describe what it demonstrates |
| - Preset buttons should be touch-friendly (larger on mobile) |
| - Applying a preset should reset the simulation |
|
|
| ### 10. Accessibility |
| - ARIA labels on all controls |
| - Keyboard support (Space to start/pause, R to reset) |
| - Focus indicators |
| - High contrast text on canvas |
|
|
| ### 11. Performance |
| - Use `requestAnimationFrame` for animations |
| - Clear canvas each frame |
| - Don't create objects in render loop |
| - Throttle slider input events if needed |
|
|
| ## Common Bugs to Avoid |
|
|
| | Bug | Cause | Solution | |
| |-----|-------|----------| |
| | Reset doesn't work | Button calls wrong function | Ensure reset function resets ALL state variables | |
| | Canvas overlap on mobile | Fixed positioning | Use flex/grid with proper responsive classes | |
| | Simulation stuck | Missing `ended` state | Track `ended` separately from `running` | |
| | Button does nothing | State logic error | Clear state machine with defined transitions | |
| | Touch issues | Small touch targets | Min 44px touch targets, larger sliders | |
|
|
| ## 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 |
|
|
| ## Object Positioning with UI Overlays |
|
|
| When calculating positions for simulation objects, account for UI overlays: |
|
|
| ```javascript |
| // BAD: Object overlaps with controls/HUD |
| const objectY = baseY - (value / maxValue) * canvas.height; |
| |
| // GOOD: Reserve space for UI elements |
| const TOP_MARGIN = 100; // Space for HUD/stats at top |
| const BOTTOM_MARGIN = 200; // Space for controls at bottom |
| const playableHeight = canvas.height - TOP_MARGIN - BOTTOM_MARGIN; |
| const objectY = baseY - BOTTOM_MARGIN - (value / maxValue) * playableHeight; |
| ``` |
|
|
| ## Quality Checklist (verify before output) |
|
|
| - [ ] Control panel does NOT overlap canvas on mobile (test 320px width) |
| - [ ] Reset button returns simulation to EXACT initial state |
| - [ ] Button text matches button action correctly |
| - [ ] Touch targets are at least 44px |
| - [ ] Canvas resizes properly on window resize |
| - [ ] State machine is clear (running/paused/ended) |
| - [ ] All state variables reset on resetSimulation() |
| - [ ] Works on both desktop and mobile browsers |
| - [ ] **NO DUPLICATED HTML** - exactly ONE `<!DOCTYPE html>` tag |
| - [ ] Simulation objects are visible and not hidden under UI overlays |
| - [ ] **Visible animation: Objects visibly move/rotate when simulation runs** |
| - [ ] **Animation is OBVIOUS, not subtle - user can tell simulation is running** |