OpenAI
o3
Final response
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Interactive Double Pendulum</title>
<style>
html,body{
margin:0;
height:100%;
background:#111;
color:#eee;
font-family:Arial,Helvetica,sans-serif;
user-select:none;
}
#ui{
position:absolute;
top:10px;
left:10px;
background:rgba(0,0,0,.7);
padding:10px 14px;
border-radius:6px;
font-size:14px;
}
#ui input[type=range]{
width:120px;
}
#ui button{
margin:4px 2px;
padding:4px 10px;
background:#444;
color:#eee;
border:none;
border-radius:4px;
cursor:pointer;
}
#ui button:hover{background:#666;}
canvas{
display:block;
width:100%;
height:100%;
}
a{color:#9cf}
</style>
</head>
<body>
<div id="ui">
<div>
θ₁:
<input id="theta1" type="range" min="-180" max="180" step="1" value="120">
<span id="theta1Val">120°</span>
</div>
<div>
θ₂:
<input id="theta2" type="range" min="-180" max="180" step="1" value=" -10">
<span id="theta2Val">-10°</span>
</div>
<div>
L₁:
<input id="len1" type="range" min="50" max="250" step="1" value="160">
<span id="len1Val">160 px</span>
</div>
<div>
L₂:
<input id="len2" type="range" min="50" max="250" step="1" value="160">
<span id="len2Val">160 px</span>
</div>
<div>
m₁:
<input id="mass1" type="range" min="1" max="10" step="1" value="4">
<span id="mass1Val">4 kg</span>
</div>
<div>
m₂:
<input id="mass2" type="range" min="1" max="10" step="1" value="4">
<span id="mass2Val">4 kg</span>
</div>
<div style="margin-top:6px;">
<button id="startBtn">Start</button>
<button id="pauseBtn">Pause</button>
<button id="resetBtn">Reset</button>
<label style="margin-left:8px;cursor:pointer;">
<input id="traceChk" type="checkbox" checked> Trace
</label>
</div>
</div>
<canvas id="c"></canvas>
<script>
(function(){
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H, originX, originY;
function resize(){
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
originX = W/2;
originY = H/4;
}
window.addEventListener('resize',resize);
resize();
// UI elements
const uiElements = {};
['theta1','theta2','len1','len2','mass1','mass2'].forEach(id=>{
uiElements[id] = document.getElementById(id);
uiElements[id+'Val'] = document.getElementById(id+'Val');
});
const traceChk = document.getElementById('traceChk');
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
// Simulation state
let params = {};
let running = false;
let theta1,theta2,omega1,omega2,l1,l2,m1,m2;
const g = 9.81;
const dt = 1/60; // simulation time step (s)
const scaleTime = 1; // simulation speed multiplier
let tracePoints = [];
function updateParamsFromUI(){
theta1 = toRad(+uiElements.theta1.value);
theta2 = toRad(+uiElements.theta2.value);
omega1 = omega2 = 0;
l1 = +uiElements.len1.value;
l2 = +uiElements.len2.value;
m1 = +uiElements.mass1.value;
m2 = +uiElements.mass2.value;
}
function refreshLabels(){
uiElements.theta1Val.textContent = uiElements.theta1.value + '°';
uiElements.theta2Val.textContent = uiElements.theta2.value + '°';
uiElements.len1Val.textContent = uiElements.len1.value + ' px';
uiElements.len2Val.textContent = uiElements.len2.value + ' px';
uiElements.mass1Val.textContent = uiElements.mass1.value + ' kg';
uiElements.mass2Val.textContent = uiElements.mass2.value + ' kg';
}
Object.values(uiElements).forEach(el=>{
if(el.tagName==='INPUT' && el.type==='range'){
el.addEventListener('input',()=>{
refreshLabels();
if(!running) updateParamsFromUI();
});
}
});
startBtn.onclick = ()=>{ running = true; };
pauseBtn.onclick = ()=>{ running = false; };
resetBtn.onclick = ()=>{
running = false;
updateParamsFromUI();
tracePoints = [];
clearCanvas();
drawPendulum();
};
function toRad(deg){ return deg*Math.PI/180; }
function clearCanvas(){
ctx.fillStyle = '#111';
ctx.fillRect(0,0,W,H);
}
function drawPendulum(){
clearCanvas();
const x1 = originX + l1*Math.sin(theta1);
const y1 = originY + l1*Math.cos(theta1);
const x2 = x1 + l2*Math.sin(theta2);
const y2 = y1 + l2*Math.cos(theta2);
if(traceChk.checked){
tracePoints.push([x2,y2]);
ctx.strokeStyle = 'rgba(180,180,255,0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
for(let i=0;i<tracePoints.length;i++){
const [tx,ty] = tracePoints[i];
if(i===0) ctx.moveTo(tx,ty);
else ctx.lineTo(tx,ty);
}
ctx.stroke();
}else{
tracePoints=[];
}
ctx.lineWidth = 2;
ctx.strokeStyle = '#eee';
ctx.fillStyle = '#eee';
// Rod 1
ctx.beginPath();
ctx.moveTo(originX,originY);
ctx.lineTo(x1,y1);
ctx.stroke();
// Rod 2
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
// Mass 1
ctx.beginPath();
ctx.arc(x1,y1,Math.max(8, m1*2),0,Math.PI*2);
ctx.fill();
// Mass 2
ctx.beginPath();
ctx.arc(x2,y2,Math.max(8, m2*2),0,Math.PI*2);
ctx.fill();
}
function rk4Step(){
const state = [theta1,theta2,omega1,omega2];
const derivs = (st)=>{
const [th1,th2,om1,om2] = st;
// Equations from https://www.myphysicslab.com/pendulum/double-pendulum-en.html
const delta = th2 - th1;
const denom1 = (m1 + m2)*l1 - m2*l1*Math.cos(delta)*Math.cos(delta);
const denom2 = (l2/l1)*denom1;
const a1 = (m2*l1*om1*om1*Math.sin(delta)*Math.cos(delta) +
m2*g*Math.sin(th2)*Math.cos(delta) +
m2*l2*om2*om2*Math.sin(delta) -
(m1 + m2)*g*Math.sin(th1)) / denom1;
const a2 = (-m2*l2*om2*om2*Math.sin(delta)*Math.cos(delta) +
(m1 + m2)*(g*Math.sin(th1)*Math.cos(delta) -
l1*om1*om1*Math.sin(delta) - g*Math.sin(th2))) / denom2;
return [om1, om2, a1, a2];
};
function addVec(a,b,dtScale=1){
return a.map((v,i)=> v + b[i]*dtScale);
}
const k1 = derivs(state);
const k2 = derivs(addVec(state,k1,dt*0.5));
const k3 = derivs(addVec(state,k2,dt*0.5));
const k4 = derivs(addVec(state,k3,dt));
const ds = state.map((v,i)=> (dt/6)*(k1[i] + 2*k2[i] + 2*k3[i] + k4[i]));
theta1 += ds[0]*scaleTime;
theta2 += ds[1]*scaleTime;
omega1 += ds[2]*scaleTime;
omega2 += ds[3]*scaleTime;
}
function loop(){
if(running) rk4Step();
drawPendulum();
requestAnimationFrame(loop);
}
// Init
refreshLabels();
updateParamsFromUI();
drawPendulum();
loop();
})();
</script>
</body>
</html>
```