Skip to content
Kiln0.10Get Kiln

Geared arms and orbiting planetary spheres

Earlier examples from earlier Kiln versions.

Historical gallery render of Orrery; see the poster provenance below.

Drag to orbit after opening. The model starts stationary.

For your engine. The download is a standard glTF 2.0 binary (GLB) with PBR metallic-roughness materials. glTF stores materials, not lighting or tone mapping, so your engine decides how they read. This 3D view tone-maps with Review Neutral, the Khronos PBR Neutral construction with a smaller glare offset (0.015 instead of 0.04), at exposure 0.9. Its Tone mapping control also shows ACES and Linear, for comparison.

Build measurements

Triangles
15,720
Estimated draws
299
Materials
10
Textures
0
Animation clips
1
Bounds X × Y × Z
1.86 × 1.75 × 1.44 m
Build warnings
0

Measurements come from this build.

Download this build

Runtime: 346,900 bytes. Original: 346,680 bytes.

These GLBs are build outputs of the MIT-licensed example source; no separate terms are stated for the builds.

Runtime provenance metadata
SHA-256 download hashes
Runtime GLB
d1cc5ab01772a4c9434f3edf4bd229ce74315aa485229f359feaaaf19d163c07
Original GLB
69400a55a315d4c2627982aac9bfe0cb4501f6fee1cce2435ced6ec9401926e7
Source
3e0b46fa8bb5c4a7a086e4a55e29f1163b32a022b8b646d26398c71254b4c755

The example source is part of the Kiln repository. Repository licence: MIT.

These GLBs are build outputs of the MIT-licensed example source; no separate terms are stated for the builds.

Recorded authorship

Gemini 3.8 Flash through Antigravity CLI (agy)

Source-header credit

Source access
Not recorded
Inherited context
Not independently recorded; source-header declarations only
Starting example
Not recorded
Human input
Header declares no hand-authored source; other intervention not recorded
Authoring review
Six-view review declared; renderer fidelity not recorded

Gallery GPU render of this exact source. Source, artifact, image hashes and camera settings are recorded alongside the poster; the artifact hash names the GLB bytes the image was rendered from, which the downloadable rebuild reproduces byte for byte only on the platform that recorded it.

Poster camera and render record

The source behind this build

orrery.kiln.js
// Authored by: gemini-3.8-flash-medium, via agy.
//
// Written by the model itself through the Kiln MCP tools: it wrote the
// program, rendered it, looked at its own six-view contact sheet, and
// revised. Not a line of it is hand-authored.
//
// Refined later, in this repository, with `kiln_edit`: the sun was lowered
// into the plane of the planets, which is the one relationship an orrery
// exists to show.
// The attribution above is for the authoring run, which had none of this in
// scope. Both passes went through the same tools; only the second one could
// see the gallery it was going into.

const meta = { name: 'Orrery', category: 'prop' };

function build() {
  const root = createRoot('Orrery');

  // Materials (PBR brass, mahogany, and planet pigments)
  const mahogany = gameMaterial(0x3a170d, { roughness: 0.45, metalness: 0.05 });
  const darkWood = gameMaterial(0x220c06, { roughness: 0.50, metalness: 0.02 });
  const polishedBrass = gameMaterial(0xd8b04a, { roughness: 0.28, metalness: 0.88 });
  const agedBrass = gameMaterial(0x8a6b2e, { roughness: 0.45, metalness: 0.82 });
  const sunGold = gameMaterial(0xffc520, { roughness: 0.20, metalness: 0.50, emissive: 0xff9a00, emissiveIntensity: 0.35 });
  const mercuryMat = gameMaterial(0x887b73, { roughness: 0.75, metalness: 0.25 });
  const venusMat = gameMaterial(0xe2caa0, { roughness: 0.45, metalness: 0.15 });
  const earthMat = gameMaterial(0x1a4674, { roughness: 0.35, metalness: 0.10 });
  const moonMat = gameMaterial(0xdedede, { roughness: 0.85, metalness: 0.05 });
  const ivory = gameMaterial(0xf7f0df, { roughness: 0.30, metalness: 0.05 });

  // Gear helper: builds disc + radially arrayed spur teeth
  function buildGear(name, teeth, radius, height, mat, parent, opts = {}) {
    const toothWidth = (Math.PI * 2 * radius / teeth) * 0.45;
    const toothDepth = radius * 0.14;
    const rimRadius = radius - toothDepth * 0.4;

    const disc = createPart(`${name}_Disc`, cylinderGeo(rimRadius, rimRadius, height, Math.max(14, teeth)), mat, {
      position: opts.position || [0, 0, 0],
      rotation: opts.rotation,
      parent
    });

    if (opts.rim) {
      createPart(`${name}_RimBezel`, torusGeo(rimRadius * 0.82, height * 0.22, 8, 16), mat, {
        position: [0, 0, 0],
        rotation: [90, 0, 0],
        parent: disc
      });
    }

    const toothGeo = boxGeo(toothDepth, height, toothWidth);
    const tooth0 = createPart(`${name}_Tooth0`, toothGeo, mat, {
      position: [rimRadius + toothDepth * 0.4, 0, 0],
      parent: disc
    });
    arrayRadial(`${name}_Tooth`, tooth0, teeth, 'y', disc);
    return disc;
  }

  // ---------------------------------------------------------------------------
  // Base: Mahogany Stand & Brass Accents (Ground sits on Y=0)
  // ---------------------------------------------------------------------------
  const baseGroup = new THREE.Object3D();
  baseGroup.name = 'BaseStructure';
  root.add(baseGroup);

  // 4 Brass feet touching ground at Y=0 (ball radius 0.03, centered at Y=0.03)
  const footPositions = [
    [0.46, 0.03, 0.46],
    [-0.46, 0.03, 0.46],
    [-0.46, 0.03, -0.46],
    [0.46, 0.03, -0.46]
  ];
  for (let i = 0; i < footPositions.length; i++) {
    const [fx, fy, fz] = footPositions[i];
    createPart(`FootBall_${i}`, sphereGeo(0.03, 14, 10), polishedBrass, {
      position: [fx, fy, fz],
      parent: baseGroup
    });
    createPart(`FootPad_${i}`, cylinderGeo(0.035, 0.04, 0.02, 14), agedBrass, {
      position: [fx, fy + 0.025, fz],
      parent: baseGroup
    });
    createPart(`FootScroll_${i}`, torusGeo(0.026, 0.007, 8, 14), polishedBrass, {
      position: [fx * 0.92, fy + 0.04, fz * 0.92],
      rotation: [45, 0, 45],
      parent: baseGroup
    });
  }

  // Stepped mahogany plinth
  createPart('BasePlinthLow', cylinderGeo(0.68, 0.72, 0.05, 26), darkWood, {
    position: [0, 0.055, 0],
    parent: baseGroup
  });
  createPart('BaseTorus', torusGeo(0.68, 0.018, 8, 24), polishedBrass, {
    position: [0, 0.08, 0],
    rotation: [90, 0, 0],
    parent: baseGroup
  });
  createPart('BasePlinthMid', cylinderGeo(0.64, 0.66, 0.05, 26), mahogany, {
    position: [0, 0.105, 0],
    parent: baseGroup
  });
  createPart('BasePlinthUpper', cylinderGeo(0.58, 0.62, 0.04, 26), mahogany, {
    position: [0, 0.15, 0],
    parent: baseGroup
  });
  createPart('BaseDeck', cylinderGeo(0.54, 0.56, 0.03, 26), darkWood, {
    position: [0, 0.185, 0],
    parent: baseGroup
  });

  // Brass Calendar / Zodiac Ring on deck
  createPart('CalendarDialPlate', cylinderGeo(0.52, 0.52, 0.008, 26), agedBrass, {
    position: [0, 0.204, 0],
    parent: baseGroup
  });
  createPart('CalendarDialRim', torusGeo(0.51, 0.010, 8, 24), polishedBrass, {
    position: [0, 0.21, 0],
    rotation: [90, 0, 0],
    parent: baseGroup
  });
  createPart('CalendarInnerRim', torusGeo(0.42, 0.008, 8, 24), polishedBrass, {
    position: [0, 0.21, 0],
    rotation: [90, 0, 0],
    parent: baseGroup
  });

  // 12 Zodiac hour studs around dial
  for (let i = 0; i < 12; i++) {
    const angle = (i * Math.PI * 2) / 12;
    const mx = Math.cos(angle) * 0.465;
    const mz = Math.sin(angle) * 0.465;
    createPart(`ZodiacStud_${i}`, cylinderGeo(0.008, 0.008, 0.015, 10), ivory, {
      position: [mx, 0.215, mz],
      parent: baseGroup
    });
  }

  // 4 Cardinal pointer markings on dial
  const cardinals = [[0.44, 0], [-0.44, 0], [0, 0.44], [0, -0.44]];
  for (let i = 0; i < cardinals.length; i++) {
    const [cx, cz] = cardinals[i];
    createPart(`CardinalPointer_${i}`, boxGeo(0.03, 0.006, 0.015), polishedBrass, {
      position: [cx, 0.212, cz],
      parent: baseGroup
    });
  }

  // ---------------------------------------------------------------------------
  // Central Mechanism Frame (Clockwork Cage)
  // ---------------------------------------------------------------------------
  const mechGroup = new THREE.Object3D();
  mechGroup.name = 'MechanismFrame';
  root.add(mechGroup);

  createPart('PillarBoss', cylinderGeo(0.18, 0.22, 0.04, 22), polishedBrass, {
    position: [0, 0.22, 0],
    parent: mechGroup
  });
  createPart('PillarBaseCollar', cylinderGeo(0.12, 0.16, 0.03, 22), agedBrass, {
    position: [0, 0.255, 0],
    parent: mechGroup
  });

  const pillarCoords = [
    [0.10, 0.10],
    [-0.10, 0.10],
    [-0.10, -0.10],
    [0.10, -0.10]
  ];
  for (let i = 0; i < pillarCoords.length; i++) {
    const [px, pz] = pillarCoords[i];
    createPart(`PillarBase_${i}`, cylinderGeo(0.025, 0.03, 0.02, 12), polishedBrass, {
      position: [px, 0.28, pz],
      parent: mechGroup
    });
    createPart(`PillarShaft_${i}`, cylinderGeo(0.016, 0.016, 0.14, 12), agedBrass, {
      position: [px, 0.36, pz],
      parent: mechGroup
    });
    createPart(`PillarTop_${i}`, cylinderGeo(0.03, 0.025, 0.02, 12), polishedBrass, {
      position: [px, 0.44, pz],
      parent: mechGroup
    });
  }

  createPart('MechPlateUpper', cylinderGeo(0.19, 0.19, 0.015, 22), polishedBrass, {
    position: [0, 0.46, 0],
    parent: mechGroup
  });

  // Central Spindle and turned decorative sleeves
  // Shortened with the sun. At 0.90 the spindle topped out at Y=1.37 to meet a
  // sun cradled at 1.34; with the sun brought down into the planet plane the
  // same rod would run straight up through the sphere and out of the top.
  createPart('CentralSpindle', cylinderGeo(0.022, 0.022, 0.72, 18), polishedBrass, {
    position: [0, 0.83, 0],
    parent: mechGroup
  });
  createPart('SpindleMidCollar', cylinderGeo(0.036, 0.036, 0.025, 18), agedBrass, {
    position: [0, 0.98, 0],
    parent: mechGroup
  });
  createPart('SpindleMidBezel', torusGeo(0.038, 0.007, 8, 18), polishedBrass, {
    position: [0, 1.15, 0],
    rotation: [90, 0, 0],
    parent: mechGroup
  });

  // Fixed central base gear
  buildGear('FixedBaseGear', 26, 0.155, 0.022, agedBrass, mechGroup, {
    position: [0, 0.48, 0],
    rim: true
  });

  // Secondary idler shaft & gears
  createPart('IdlerShaft', cylinderGeo(0.010, 0.010, 0.38, 10), polishedBrass, {
    position: [0.12, 0.65, 0.08],
    parent: mechGroup
  });
  buildGear('IdlerGear1', 16, 0.075, 0.018, agedBrass, mechGroup, {
    position: [0.12, 0.54, 0.08]
  });
  buildGear('IdlerGear2', 14, 0.065, 0.018, agedBrass, mechGroup, {
    position: [0.12, 0.66, 0.08]
  });
  buildGear('IdlerGear3', 12, 0.055, 0.018, agedBrass, mechGroup, {
    position: [0.12, 0.78, 0.08]
  });

  // ---------------------------------------------------------------------------
  // Winding Crank Mechanism (Moving joint Joint_Crank)
  // ---------------------------------------------------------------------------
  createPart('CrankBracket', boxGeo(0.09, 0.035, 0.04), agedBrass, {
    position: [0.14, 0.36, 0],
    parent: mechGroup
  });
  createPart('CrankBearing', boxGeo(0.05, 0.05, 0.06), agedBrass, {
    position: [0.18, 0.36, 0],
    parent: mechGroup
  });

  const crankJoint = createPivot('Crank', [0.22, 0.36, 0], root);
  createPart('CrankAxle', cylinderXGeo(0.01, 0.01, 0.10, 10), polishedBrass, {
    position: [0.05, 0, 0],
    parent: crankJoint
  });

  buildGear('CrankPinion', 12, 0.048, 0.016, agedBrass, crankJoint, {
    position: [-0.02, 0, 0],
    rotation: [0, 0, 90]
  });

  createPart('CrankArm', boxGeo(0.015, 0.11, 0.02), polishedBrass, {
    position: [0.10, 0.045, 0],
    parent: crankJoint
  });
  createPart('CrankHubCollar', cylinderXGeo(0.018, 0.018, 0.02, 12), agedBrass, {
    position: [0.10, 0, 0],
    parent: crankJoint
  });
  createPart('CrankPin', cylinderXGeo(0.008, 0.008, 0.06, 10), polishedBrass, {
    position: [0.13, 0.09, 0],
    parent: crankJoint
  });
  createPart('CrankIvoryKnob', cylinderXGeo(0.016, 0.012, 0.05, 14), ivory, {
    position: [0.145, 0.09, 0],
    parent: crankJoint
  });

  // ---------------------------------------------------------------------------
  // Arm 3: Earth & Moon (Outermost planet, lowest arm tier at Y=0.54, R3 = 0.94m)
  // By placing the outermost arm at the lowest tier, its long arm sweeps below
  // Venus and Mercury, completely preventing mechanical binding!
  // ---------------------------------------------------------------------------
  const earthJoint = createPivot('EarthArm', [0, 0.54, 0], root);

  createPart('EarthCollar', cylinderGeo(0.046, 0.046, 0.035, 14), polishedBrass, {
    position: [0, 0, 0],
    parent: earthJoint
  });
  buildGear('EarthGear', 28, 0.165, 0.02, agedBrass, earthJoint, {
    position: [0, -0.01, 0],
    rim: true
  });

  beamBetween('EarthRailLower', [0, 0, 0], [0.94, 0, 0], 0.008, polishedBrass, { parent: earthJoint });
  beamBetween('EarthRailUpper', [0.05, 0.045, 0], [0.94, 0.045, 0], 0.007, polishedBrass, { parent: earthJoint });

  const strutXs = [0.24, 0.44, 0.64, 0.84];
  for (let i = 0; i < strutXs.length; i++) {
    const sx = strutXs[i];
    beamBetween(`EarthStrut_${i}`, [sx, 0, 0], [sx, 0.045, 0], 0.005, agedBrass, { parent: earthJoint });
    beamBetween(`EarthDiag_${i}`, [sx - 0.08, 0, 0], [sx, 0.045, 0], 0.004, agedBrass, { parent: earthJoint });
  }

  beamBetween('EarthCounterArm', [0, 0.02, 0], [-0.30, 0.02, 0], 0.009, polishedBrass, { parent: earthJoint });
  createPart('EarthWeightBase', cylinderGeo(0.048, 0.052, 0.05, 16), agedBrass, {
    position: [-0.30, 0.02, 0],
    parent: earthJoint
  });
  createPart('EarthWeightFinial', sphereGeo(0.032, 14, 10), polishedBrass, {
    position: [-0.35, 0.02, 0],
    parent: earthJoint
  });

  // Earth riser column at R3 = 0.94m rising up to Y = 0.96 (world Y = 1.50)
  beamBetween('EarthRiserPillar', [0.94, 0, 0], [0.94, 0.96, 0], 0.010, polishedBrass, { parent: earthJoint });
  createPart('EarthGearboxHousing', cylinderGeo(0.038, 0.042, 0.045, 14), agedBrass, {
    position: [0.94, 0.97, 0],
    parent: earthJoint
  });
  buildGear('EarthMiniGear', 14, 0.038, 0.014, polishedBrass, earthJoint, {
    position: [0.94, 1.00, 0]
  });

  // Miniature Armillary Gimbal around Earth (23.5 degrees axial tilt)
  const meridianTilt = 23.5;
  createPart('EarthMeridianRing', torusGeo(0.12, 0.007, 8, 20), polishedBrass, {
    position: [0.94, 1.08, 0],
    rotation: [0, 0, meridianTilt],
    parent: earthJoint
  });
  createPart('EarthEquatorRing', torusGeo(0.11, 0.005, 8, 18), polishedBrass, {
    position: [0.94, 1.08, 0],
    rotation: [90, 0, meridianTilt],
    parent: earthJoint
  });
  createPart('EarthAxisPin', cylinderGeo(0.005, 0.005, 0.26, 10), agedBrass, {
    position: [0.94, 1.08, 0],
    rotation: [0, 0, meridianTilt],
    parent: earthJoint
  });
  createPart('EarthSphere', sphereGeo(0.076, 22, 16), earthMat, {
    position: [0.94, 1.08, 0],
    parent: earthJoint
  });

  // Moon Sub-assembly (pivots around Earth, parented to Joint_EarthArm)
  const moonJoint = createPivot('Moon', [0.94, 1.02, 0], earthJoint);

  beamBetween('MoonArmBeam', [0, 0, 0], [0.18, 0.04, 0], 0.004, polishedBrass, { parent: moonJoint });
  beamBetween('MoonRiser', [0.18, 0.04, 0], [0.18, 0.06, 0], 0.003, polishedBrass, { parent: moonJoint });
  beamBetween('MoonCounterArm', [0, 0, 0], [-0.05, 0, 0], 0.003, agedBrass, { parent: moonJoint });
  createPart('MoonWeightBall', sphereGeo(0.012, 12, 8), agedBrass, {
    position: [-0.05, 0, 0],
    parent: moonJoint
  });
  createPart('MoonSphere', sphereGeo(0.024, 14, 10), moonMat, {
    position: [0.18, 0.06, 0],
    parent: moonJoint
  });

  // ---------------------------------------------------------------------------
  // Arm 2: Venus (Middle planet, middle arm tier at Y=0.66, R2 = 0.64m)
  // Reaches to R=0.64, well inside Earth's riser at R=0.94
  // ---------------------------------------------------------------------------
  const venusJoint = createPivot('Venus', [0, 0.66, 0], root);
  venusJoint.rotation.y = THREE.MathUtils.degToRad(210);

  createPart('VenusCollar', cylinderGeo(0.040, 0.040, 0.035, 14), polishedBrass, {
    position: [0, 0, 0],
    parent: venusJoint
  });
  buildGear('VenusGear', 22, 0.135, 0.02, agedBrass, venusJoint, {
    position: [0, -0.01, 0],
    rim: true
  });

  beamBetween('VenusArmBeam', [0, 0, 0], [0.64, 0, 0], 0.008, polishedBrass, { parent: venusJoint });

  createPart('VenusScroll', torusGeo(0.06, 0.005, 8, 16), polishedBrass, {
    position: [0.32, -0.052, 0],
    rotation: [0, 0, 0],
    parent: venusJoint
  });

  beamBetween('VenusCounterArm', [0, 0, 0], [-0.22, 0, 0], 0.007, polishedBrass, { parent: venusJoint });
  createPart('VenusWeightCylinder', cylinderGeo(0.035, 0.035, 0.04, 14), agedBrass, {
    position: [-0.22, 0, 0],
    parent: venusJoint
  });
  createPart('VenusWeightFinial', sphereGeo(0.022, 14, 10), polishedBrass, {
    position: [-0.25, 0, 0],
    parent: venusJoint
  });

  beamBetween('VenusRiser', [0.64, 0, 0], [0.64, 0.74, 0], 0.007, polishedBrass, { parent: venusJoint });
  createPart('VenusCup', coneGeo(0.022, 0.03, 12), agedBrass, {
    position: [0.64, 0.755, 0],
    parent: venusJoint
  });
  createPart('VenusSphere', sphereGeo(0.066, 20, 14), venusMat, {
    position: [0.64, 0.82, 0],
    parent: venusJoint
  });

  // ---------------------------------------------------------------------------
  // Arm 1: Mercury (Innermost planet, top arm tier at Y=0.78, R1 = 0.36m)
  // Sits highest in the gear stack, perfectly clear of Venus (R=0.64) and Earth (R=0.94)
  // ---------------------------------------------------------------------------
  const mercuryJoint = createPivot('Mercury', [0, 0.78, 0], root);
  mercuryJoint.rotation.y = THREE.MathUtils.degToRad(75);

  createPart('MercCollar', cylinderGeo(0.036, 0.036, 0.035, 14), polishedBrass, {
    position: [0, 0, 0],
    parent: mercuryJoint
  });
  buildGear('MercGear', 18, 0.105, 0.02, agedBrass, mercuryJoint, {
    position: [0, -0.01, 0],
    rim: true
  });

  beamBetween('MercArmBeam', [0, 0, 0], [0.36, 0, 0], 0.007, polishedBrass, { parent: mercuryJoint });

  createPart('MercScroll', torusGeo(0.055, 0.004, 8, 16), polishedBrass, {
    position: [0.18, -0.048, 0],
    rotation: [0, 0, 0],
    parent: mercuryJoint
  });

  beamBetween('MercCounterArm', [0, 0, 0], [-0.14, 0, 0], 0.006, polishedBrass, { parent: mercuryJoint });
  createPart('MercWeightBall', sphereGeo(0.028, 14, 10), agedBrass, {
    position: [-0.14, 0, 0],
    parent: mercuryJoint
  });

  beamBetween('MercRiser', [0.36, 0, 0], [0.36, 0.50, 0], 0.006, polishedBrass, { parent: mercuryJoint });
  createPart('MercCup', coneGeo(0.016, 0.025, 12), agedBrass, {
    position: [0.36, 0.51, 0],
    parent: mercuryJoint
  });
  createPart('MercurySphere', sphereGeo(0.042, 16, 12), mercuryMat, {
    position: [0.36, 0.56, 0],
    parent: mercuryJoint
  });

  // ---------------------------------------------------------------------------
  // The Sun.
  //
  // It used to sit at Y=1.66, on top of a 0.90 m spindle, with the three planets
  // banded between 1.34 and 1.62 below and around it. Every part was individually
  // fine and the whole thing read as a table lamp: the one relationship an orrery
  // exists to show is that the planets go ROUND the sun, and a sun mounted above
  // them shows the opposite. Dropped to 1.48, which is the mean of Mercury (1.34),
  // Venus (1.48) and Earth (1.62), so the orbits now pass through it.
  // ---------------------------------------------------------------------------
  const sunGroup = new THREE.Object3D();
  sunGroup.name = 'SunAssembly';
  root.add(sunGroup);

  createPart('SunFinialBase', cylinderGeo(0.048, 0.065, 0.08, 18), polishedBrass, {
    position: [0, 1.16, 0],
    parent: sunGroup
  });
  createPart('SunNeck', cylinderGeo(0.030, 0.045, 0.08, 18), agedBrass, {
    position: [0, 1.24, 0],
    parent: sunGroup
  });
  createPart('SunCup', cylinderGeo(0.085, 0.04, 0.05, 20), polishedBrass, {
    position: [0, 1.305, 0],
    parent: sunGroup
  });

  // 16 Radiant Sunburst Rays
  for (let i = 0; i < 16; i++) {
    const angle = (i * Math.PI * 2) / 16;
    const rx = Math.cos(angle) * 0.085;
    const rz = Math.sin(angle) * 0.085;
    createPart(`SunRay_${i}`, coneGeo(0.009, 0.055, 8), sunGold, {
      position: [rx, 1.32, rz],
      rotation: [Math.sin(angle) * 35, 0, -Math.cos(angle) * 35],
      parent: sunGroup
    });
  }

  createPart('SunSphere', sphereGeo(0.165, 24, 18), sunGold, {
    position: [0, 1.48, 0],
    parent: sunGroup
  });

  return root;
}

function animate() {
  const duration = 12;

  // Track 1: Hand Crank (turns on X axis, 12 revolutions in 12s)
  const crankKeys = [];
  const crankSteps = 48;
  for (let i = 0; i <= crankSteps; i++) {
    crankKeys.push({
      time: (i * duration) / crankSteps,
      rotation: [(i * 90) % 360, 0, 0]
    });
  }

  // Track 2: Mercury (starts at 75°, completes 4 revolutions in 12s)
  const mercKeys = [];
  const mercSteps = 16;
  const mercBase = 75;
  for (let i = 0; i <= mercSteps; i++) {
    mercKeys.push({
      time: (i * duration) / mercSteps,
      rotation: [0, (mercBase + i * 90) % 360, 0]
    });
  }

  // Track 3: Venus (starts at 210°, completes 2 revolutions in 12s)
  const venusKeys = [];
  const venusSteps = 8;
  const venusBase = 210;
  for (let i = 0; i <= venusSteps; i++) {
    venusKeys.push({
      time: (i * duration) / venusSteps,
      rotation: [0, (venusBase + i * 90) % 360, 0]
    });
  }

  // Track 4: Earth (starts at 0°, completes 1 revolution in 12s)
  const earthKeys = [];
  const earthSteps = 4;
  for (let i = 0; i <= earthSteps; i++) {
    earthKeys.push({
      time: (i * duration) / earthSteps,
      rotation: [0, (i * 90) % 360, 0]
    });
  }

  // Track 5: Moon (orbits Earth, completes 6 revolutions in 12s)
  const moonKeys = [];
  const moonSteps = 24;
  for (let i = 0; i <= moonSteps; i++) {
    moonKeys.push({
      time: (i * duration) / moonSteps,
      rotation: [0, (i * 90) % 360, 0]
    });
  }

  return [
    createClip('OrreryMotion', duration, [
      rotationTrack('Joint_Crank', crankKeys),
      rotationTrack('Joint_Mercury', mercKeys),
      rotationTrack('Joint_Venus', venusKeys),
      rotationTrack('Joint_EarthArm', earthKeys),
      rotationTrack('Joint_Moon', moonKeys)
    ])
  ];
}

Brief and retained revisions

No brief or earlier revisions are recorded. The source download contains the version built for this page.