---
format: typebulb/v1
name: Rocket Balancer
---

**code.tsx**

```tsx
import React, { useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

// ROCKET PHYSICS ---

// Linearized inverted rod with LQR. With M=0, m=1, d=1, equations simplify significantly.
const G = 9.81, MAX_ANGLE = 0.4;
const K = [1, 2.06318, 25.7842, 4.50866]; // Shared LQR gains for both axes

// Scaling and timing
const PHYSICS_DT = 1 / 60;
const POSE_SCALE = 1.5;

function createPhysics(alpha = 0, beta = 0) {
  // State: [x, xDot, y, yDot, alpha, alphaDot, beta, betaDot]
  let s = [0, 0, 0, 0, alpha, 0, beta, 0];

  const deriv = (s: number[], fx: number, fy: number) => [
    s[1], -3 * G * s[4] + 4 * fx,
    s[3], -3 * G * s[6] + 4 * fy,
    s[5], 6 * G * s[4] - 6 * fx,
    s[7], 6 * G * s[6] - 6 * fy
  ];

  const lqr = (s: number[]): [number, number] => [
    K[0] * s[0] + K[1] * s[1] + K[2] * s[4] + K[3] * s[5],
    K[0] * s[2] + K[1] * s[3] + K[2] * s[6] + K[3] * s[7]
  ];

  return {
    step(dt: number, distFx = 0, distFy = 0) {
      const u = (st: number[]): [number, number] => { const [cx, cy] = lqr(st); return [cx + distFx, cy + distFy]; };
      const add = (a: number[], b: number[], k = 1) => a.map((v, i) => v + k * b[i]);
      const k1 = deriv(s, ...u(s)), s2 = add(s, k1, dt / 2);
      const k2 = deriv(s2, ...u(s2)), s3 = add(s, k2, dt / 2);
      const k3 = deriv(s3, ...u(s3)), s4 = add(s, k3, dt);
      const k4 = deriv(s4, ...u(s4));
      s = s.map((v, i) => v + (dt / 6) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]));
      // Clamp angles
      const r = Math.hypot(s[4], s[6]), maxR = Math.sin(MAX_ANGLE);
      if (r > maxR) { s[4] *= maxR / r; s[6] *= maxR / r; }
    },

    getPose() {
      const [x,, y,, a,, b] = s;

      // For small angles: theta ≈ sqrt(alpha^2 + beta^2), as per Beckman's paper
      const theta = Math.min(Math.hypot(a, b), MAX_ANGLE);
      const phi = Math.atan2(b, a);

      const st = Math.sin(theta);
      const ct = Math.cos(theta);
      const sp = Math.sin(phi), cp = Math.cos(phi);
      return { base: [x, y], tip: [x + st * cp, y + st * sp, ct] };
    }
  };
}

// APP ---

// Wind simulation
const WIND_STRENGTH_MULTIPLIER = 15;
const WIND_SMOOTHING = 0.08;

const flameCols = [0xffff00, 0xff8800, 0xff3300];

// Rocket dimensions
const ROCKET_RADIUS = 0.225;
const ROCKET_BODY_HEIGHT = 1.5;
const ROCKET_TOP_HEIGHT = 0.5;
const ROCKET_BASE_Y = -1;
const FIN_HEIGHT = 0.75;
const FIN_DEPTH = 0.06;
const NOSE_CONE_STRETCH = 4;
const ROCKET_GROUND_HEIGHT = 2;
const ROCKET_BODY_Y = ROCKET_BASE_Y + ROCKET_BODY_HEIGHT / 2;
const ROCKET_TOP_Y = ROCKET_BASE_Y + ROCKET_BODY_HEIGHT + ROCKET_TOP_HEIGHT / 2;
const NOSE_CONE_Y = ROCKET_BASE_Y + ROCKET_BODY_HEIGHT + ROCKET_TOP_HEIGHT;

// Flame and particle effects
const FLAME_COUNT = 12;
const MAX_PARTICLES = 60;
const PARTICLE_SPAWN_INTERVAL = 0.03;
const PARTICLE_MIN_LIFE = 0.6;
const PARTICLE_MAX_LIFE = 1.1;

// Wind vane position
const WIND_VANE_X = -5;
const WIND_VANE_POLE_HEIGHT = 4;

function App() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [wind, setWind] = useState(0.5);
  const windRef = useRef(wind);
  const [dark, setDark] = useState(() => document.documentElement.dataset.theme === 'dark');

  useEffect(() => {
    const obs = new MutationObserver(() => setDark(document.documentElement.dataset.theme === 'dark'));
    obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
    return () => obs.disconnect();
  }, []);

  useEffect(() => { windRef.current = wind; }, [wind]);

  useEffect(() => {
    if (!containerRef.current) return;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 100);
    camera.position.set(5, 3.33, 5);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(innerWidth, innerHeight);
    renderer.shadowMap.enabled = true;
    containerRef.current.appendChild(renderer.domElement);

    const mat = (c: number, em = 0.3) => new THREE.MeshStandardMaterial({
      color: c, metalness: 0.6, roughness: 0.4, emissive: dark ? c : 0, emissiveIntensity: dark ? em : 0
    });

    scene.add(new THREE.Mesh(new THREE.SphereGeometry(90, 32, 32), new THREE.ShaderMaterial({
      uniforms: { t: { value: new THREE.Color(dark ? 0x000510 : 0x4a90d9) }, b: { value: new THREE.Color(dark ? 0x1a3050 : 0xe0f4ff) } },
      vertexShader: `varying vec3 p; void main(){p=(modelMatrix*vec4(position,1)).xyz;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1);}`,
      fragmentShader: `uniform vec3 t,b;varying vec3 p;void main(){gl_FragColor=vec4(mix(b,t,smoothstep(0.,.8,normalize(p).y)),1.);}`,
      side: THREE.BackSide
    })));

    scene.add(new THREE.AmbientLight(0xffffff, dark ? 0.3 : 0.4));
    const sun = new THREE.DirectionalLight(dark ? 0x8888ff : 0xffffff, 0.8);
    sun.position.set(10, 20, 10); sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    Object.assign(sun.shadow.camera, { left: -20, right: 20, top: 20, bottom: -20 });
    scene.add(sun);

    const ground = new THREE.Mesh(new THREE.PlaneGeometry(50, 50), new THREE.MeshStandardMaterial({ color: dark ? 0x4a7c1a : 0x4a7c59, roughness: 0.8 }));
    ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground);
    const grid = new THREE.GridHelper(50, 50, 0x000000, 0x333333); grid.position.y = 0.01; scene.add(grid);

    if (dark) {
      const pos = new Float32Array(3000);
      for (let i = 0; i < 3000; i += 3) {
        const r = 70 + Math.random() * 20, t = Math.random() * Math.PI / 2, p = Math.random() * Math.PI * 2;
        pos[i] = r * Math.sin(t) * Math.cos(p); pos[i + 1] = r * Math.cos(t); pos[i + 2] = r * Math.sin(t) * Math.sin(p);
      }
      const stars = new THREE.BufferGeometry(); stars.setAttribute('position', new THREE.BufferAttribute(pos, 3));
      scene.add(new THREE.Points(stars, new THREE.PointsMaterial({ color: 0xffffff, size: 0.1 })));
    }

    const rocket = new THREE.Group();
    [[new THREE.CylinderGeometry(ROCKET_RADIUS, ROCKET_RADIUS, ROCKET_BODY_HEIGHT, 16), 0xd32f2f, ROCKET_BODY_Y],
     [new THREE.CylinderGeometry(ROCKET_RADIUS, ROCKET_RADIUS, ROCKET_TOP_HEIGHT, 16), 0xffffff, ROCKET_TOP_Y],
    ].forEach(([g, c, y]) => {
      const m = new THREE.Mesh(g as THREE.BufferGeometry, mat(c as number));
      m.position.y = y as number; m.castShadow = true; rocket.add(m);
    });

    const noseCone = new THREE.Mesh(
      new THREE.SphereGeometry(ROCKET_RADIUS, 16, 16, 0, Math.PI * 2, 0, Math.PI / 2),
      mat(0xffffff)
    );
    noseCone.position.y = NOSE_CONE_Y;
    noseCone.scale.y = NOSE_CONE_STRETCH;
    noseCone.castShadow = true;
    rocket.add(noseCone);

    const finG = new THREE.ExtrudeGeometry(new THREE.Shape().moveTo(0, 0).lineTo(ROCKET_RADIUS, 0).lineTo(0, FIN_HEIGHT), { depth: FIN_DEPTH, bevelEnabled: false });
    finG.translate(0, 0, -FIN_DEPTH / 2);
    for (let i = 0; i < 3; i++) {
      const w = new THREE.Group(), f = new THREE.Mesh(finG, mat(0x1976d2));
      f.position.set(ROCKET_RADIUS, ROCKET_BASE_Y, 0); f.castShadow = true;
      w.add(f); w.rotation.y = i * Math.PI * 2 / 3; rocket.add(w);
    }

    const flames = new THREE.Group();
    const coreFlames = Array.from({ length: FLAME_COUNT }, () => {
      const a = Math.random() * Math.PI * 2, r = Math.random() * ROCKET_RADIUS;
      const m = new THREE.Mesh(new THREE.ConeGeometry(0.05 + Math.random() * 0.1, 0.3 + Math.random() * 0.6, 6),
        new THREE.MeshBasicMaterial({ color: flameCols[~~(Math.random() * 3)], transparent: true, opacity: 0.7 }));
      m.position.set(Math.cos(a) * r, ROCKET_BASE_Y - Math.random() * 0.5, Math.sin(a) * r);
      flames.add(m);
      return { m, baseY: m.position.y, spd: 0.5 + Math.random() };
    });

    const light = new THREE.PointLight(0xff6600, 2, 4); light.position.y = ROCKET_BASE_Y - 0.2; flames.add(light);
    const particles: { m: THREE.Mesh; life: number; maxLife: number; vel: THREE.Vector3 }[] = [];
    rocket.add(flames); scene.add(rocket);

    const vane = new THREE.Group();
    const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, WIND_VANE_POLE_HEIGHT, 8), mat(0x444444)); pole.position.y = WIND_VANE_POLE_HEIGHT / 2; pole.castShadow = true; vane.add(pole);
    const arrow = new THREE.Group();
    const am = mat(0xff6b6b);
    [new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.8, 8), am), new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.3, 8), am), new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.15, 0.02), am)]
      .forEach((m, i) => { m.rotation.z = [Math.PI / 2, -Math.PI / 2, 0][i]; m.position.x = [0, 0.55, -0.55][i]; arrow.add(m); });
    arrow.position.y = WIND_VANE_POLE_HEIGHT; vane.add(arrow); vane.position.x = WIND_VANE_X; scene.add(vane);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; controls.minDistance = 3; controls.maxDistance = 20; controls.maxPolarAngle = Math.PI / 2;

    const physics = createPhysics(0.15, 0.1);
    let t = 0, spawnTimer = 0, animId: number;

    const windForce = { fx: 0, fy: 0 };

    const updateWind = (dt: number) => {
      const currentWind = windRef.current;
      if (currentWind <= 0) {
        windForce.fx *= 0.95;
        windForce.fy *= 0.95;
        return;
      }

      const strength = currentWind * WIND_STRENGTH_MULTIPLIER;
        // Mix 3 frequencies: slow drift + gusts + turbulence (natural fractal-like variation)
        const nx = Math.sin(t * 0.3) * 0.5 + Math.sin(t * 1.8) * 0.3 + Math.sin(t * 4.2) * 0.2;
        const ny = Math.cos(t * 0.25) * 0.5 + Math.cos(t * 1.5) * 0.3 + Math.cos(t * 3.8) * 0.2;

      windForce.fx += (strength * nx - windForce.fx) * WIND_SMOOTHING;
      windForce.fy += (strength * ny - windForce.fy) * WIND_SMOOTHING;
    };

    const animate = () => {
      animId = requestAnimationFrame(animate);
      t += 0.01;

      updateWind(1 / 60);
      arrow.rotation.y = Math.atan2(windForce.fy, windForce.fx);
      physics.step(PHYSICS_DT, windForce.fx, windForce.fy);
      const { base: [x, y], tip: [tx, ty, tz] } = physics.getPose();

      // Hover above the cart position - offset so flame are well above ground
      const baseVec = new THREE.Vector3(x * POSE_SCALE, ROCKET_GROUND_HEIGHT, y * POSE_SCALE);
      const tipVec = new THREE.Vector3(tx * POSE_SCALE, tz * POSE_SCALE, ty * POSE_SCALE);
      tipVec.y += ROCKET_GROUND_HEIGHT; // Match the base offset

      // Position at base and lean along the rod direction
      rocket.position.copy(baseVec);

      // Orient rocket so its +Y axis points from base to tip
      const rodDir = new THREE.Vector3().subVectors(tipVec, baseVec).normalize();
      rocket.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), rodDir);

      coreFlames.forEach(({ m, baseY, spd }) => {
        const f = t * 10 * spd;
        m.scale.set(0.8 + Math.sin(f * 1.7) * 0.3, 1 + Math.sin(f) * 0.4, 0.8 + Math.sin(f * 1.7) * 0.3);
        m.position.y = baseY + Math.sin(f * 1.3) * 0.1;
      });
      light.intensity = 2 + Math.sin(t * 15) * 0.5;

      if ((spawnTimer += PHYSICS_DT) > PARTICLE_SPAWN_INTERVAL && particles.length < MAX_PARTICLES) {
        spawnTimer = 0;
        const a = Math.random() * Math.PI * 2, r = Math.random() * ROCKET_RADIUS * 1.67;
        const m = new THREE.Mesh(new THREE.SphereGeometry(0.06 + Math.random() * 0.08, 6, 6),
          new THREE.MeshBasicMaterial({ color: flameCols[~~(Math.random() * 3)], transparent: true, opacity: 0.8 }));
        m.position.set(Math.cos(a) * r, ROCKET_BASE_Y - Math.random() * 0.3, Math.sin(a) * r);
        flames.add(m);
        particles.push({ m, life: 0, maxLife: PARTICLE_MIN_LIFE + Math.random() * (PARTICLE_MAX_LIFE - PARTICLE_MIN_LIFE), vel: new THREE.Vector3((Math.random() - 0.5) * 3, -(0.5 + Math.random()), (Math.random() - 0.5) * 3) });
      }

      for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];
        if ((p.life += PHYSICS_DT) > p.maxLife) { flames.remove(p.m); particles.splice(i, 1); continue; }
        const r = p.life / p.maxLife;
        p.m.position.addScaledVector(p.vel, PHYSICS_DT);
        (p.m.material as THREE.MeshBasicMaterial).opacity = 0.8 * (1 - r);
        p.m.scale.setScalar(1 + r * 1.2);
      }

      controls.update();
      camera.position.y = Math.max(camera.position.y, 0.5);
      renderer.render(scene, camera);
    };
    animate();

    const resize = () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); };
    addEventListener("resize", resize);

    return () => {
      cancelAnimationFrame(animId);
      removeEventListener("resize", resize);
      controls.dispose();
      containerRef.current?.removeChild(renderer.domElement);
      renderer.dispose();
    };
  }, [dark]);

  return (
    <>
      <div ref={containerRef} />
      <div className="controls">
        <div>
          <h3 className="controls-title">Linear Quadratic Regulator Control of Sliding-Base Inverted Rod</h3>
          <a href="https://community.wolfram.com/groups/-/m/t/3581351" target="_blank" rel="noopener noreferrer" className="controls-link">Explanation</a>
        </div>
        <label>
          <span>Wind Intensity</span>
          <input type="range" min="0" max="1" step="0.01" value={wind} onChange={e => setWind(+e.target.value)} />
          <span className="value">{Math.round(wind * 100)}%</span>
        </label>
      </div>
    </>
  );
}

createRoot(document.getElementById("root")!).render(<App />);
```
**styles.css**

```css
:root {
  --bg-panel: rgba(255, 255, 255, 0.15);
  --text-primary: #ffffff;
  --border-color: #ddd;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  overflow: hidden;
  font-family: system-ui, -apple-system, sans-serif;
}

#root {
  width: 100vw;
  height: 100vh;
}

canvas {
  display: block;
}
.controls > div {
  margin-bottom: 16px;
}

.controls-link {
  display: block;
  margin-top: 4px;
  color: #2196f3;
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
}

.controls-link:hover {
  text-decoration: underline;
}

.controls {
  position: absolute;
  bottom: 20px;
  right: 20px;
  background: transparent;
  border-radius: 8px;
  padding: 16px 20px;
  min-width: 250px;
  max-width: 300px;
}

.controls-title {
  margin: 0 0 0 0;
  font-size: 16px;
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-primary);
  line-height: 1.3;
  white-space: normal;
  word-wrap: break-word;
  overflow-wrap: break-word;
}

.controls label {
  display: flex;
  flex-direction: column;
  gap: 8px;
  color: var(--text-primary);
  font-size: 14px;
  font-weight: 500;
}

.controls input[type="range"] {
  width: 100%;
  height: 6px;
  border-radius: 3px;
  background: var(--border-color);
  outline: none;
  -webkit-appearance: none;
}

.controls input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #2196f3;
  cursor: pointer;
}

.controls input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #2196f3;
  cursor: pointer;
  border: none;
}

.controls .value {
  text-align: right;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.8);
}

html[data-theme="dark"] {
  --bg-panel: rgba(30, 30, 30, 0.15);
  --text-primary: #e0e0e0;
  --border-color: #444;
}

html[data-theme="dark"] .controls .value {
  color: rgba(255, 255, 255, 0.7);
}

```
**index.html**

```html
<div id="root"></div>
```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.3",
    "react-dom": "^19.2.3",
    "three": "^0.181.2"
  },
  "description": "Linear Quadratic Regulator Control of Sliding-Base Inverted Rod, based on physics by Brian Beckman."
}
```