Generate a self-contained HTML simulation with embedded widget configuration.
Your output must be a complete HTML document with:
<script type="application/json" id="widget-config"> tag{
"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 } }
]
}
Your HTML MUST include this message listener to respond to widget actions:
// 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);
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")
min-height for canvas to ensure it's visible on mobileExample mobile-safe layout:
<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>
Correct implementation:
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;
}
running, paused, endedtouch-action: manipulation to prevent double-tap zoomtouch-action: none on canvas for custom gesture handlingResizeObserver or window resize eventmax-heightWhen the user clicks "启动" (Start), there MUST be OBVIOUS visual animation.
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
// Earth is static 2D circle, only time number changes
// User clicks "Start" → Nothing visibly moves → Confusing!
// Earth rotates, sun position moves, day/night boundary shifts
// User clicks "Start" → Earth visibly spins → Satisfying!
requestAnimationFrame for animations| 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 |
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
When calculating positions for simulation objects, account for UI overlays:
// 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;
<!DOCTYPE html> tag