Gears on a mobius strip create an illusion where because the entire strip is rotating, alternating gears seem to be turning faster/slower.
---
format: typebulb/v1
name: Mobius Gears
---
**code.tsx**
```tsx
import React, { useMemo, useRef } from "react";
import { createRoot } from "react-dom/client";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { PerspectiveCamera } from "@react-three/drei";
import * as THREE from "three";
const TAU = Math.PI * 2;
const GEAR_COUNT = 19; // Must be odd for proper Möbius interlocking
const TEETH_PER_GEAR = 11;
const Gear = ({ index, total, radius, gearRadius, teeth }: { index: number, total: number, radius: number, gearRadius: number, teeth: number }) => {
const meshRef = useRef<THREE.Mesh>(null);
const { shape, dir, position, quaternion, initialRotation, extrudeDepth } = useMemo(() => {
const u = (index / total) * TAU;
const dir = index % 2 === 0 ? 1 : -1;
const depth = gearRadius * 0.27;
const holeR = gearRadius * 0.23;
const innerR = gearRadius - depth;
const step = TAU / teeth;
// Build Gear Shape
const s = new THREE.Shape();
for (let i = 0; i < teeth; i++) {
const a = i * step;
const x = innerR * Math.cos(a);
const y = innerR * Math.sin(a);
if (i === 0) s.moveTo(x, y);
else s.lineTo(x, y);
s.lineTo(innerR * Math.cos(a + step * 0.25), innerR * Math.sin(a + step * 0.25));
s.lineTo(gearRadius * Math.cos(a + step * 0.3), gearRadius * Math.sin(a + step * 0.3));
s.lineTo(gearRadius * Math.cos(a + step * 0.7), gearRadius * Math.sin(a + step * 0.7));
s.lineTo(innerR * Math.cos(a + step * 0.75), innerR * Math.sin(a + step * 0.75));
}
s.closePath();
s.holes.push(new THREE.Path().absarc(0, 0, holeR, 0, TAU, false));
// Calculate Mobius Position & Orientation
const pos = new THREE.Vector3(radius * Math.cos(u), radius * Math.sin(u), 0);
const T = new THREE.Vector3(-Math.sin(u), Math.cos(u), 0);
const hu = u / 2;
const W = new THREE.Vector3(Math.cos(hu) * Math.cos(u), Math.cos(hu) * Math.sin(u), Math.sin(hu)).normalize();
const nVec = new THREE.Vector3().crossVectors(T, W).normalize();
const quat = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(T, W, nVec));
// Tooth phasing must NOT drift continuously with u, otherwise some adjacent
// pairs will end up tooth-on-tooth (overlap).
//
// With an odd tooth count, opposite contact points (+x vs -x) naturally
// produce the needed half-pitch relationship, so keep all gears on the
// same phase.
//
// With an even tooth count, offset every other gear by half a tooth pitch.
const meshingOffset = (teeth % 2 === 0 && dir < 0) ? step / 2 : 0;
return {
shape: s,
dir,
position: pos,
quaternion: quat,
initialRotation: meshingOffset,
extrudeDepth: gearRadius * 0.17
};
}, [index, total, radius, gearRadius, teeth]);
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.z += delta * 1.5 * dir;
}
});
return (
<group position={position} quaternion={quaternion}>
{/* ExtrudeGeometry extrudes in +Z from 0..depth (not centered).
Center it around the local origin so neighboring gears share the same mid-plane,
avoiding a subtle "kink" where two meshing gears look axially offset. */}
<mesh ref={meshRef} rotation={[0, 0, initialRotation]} position={[0, 0, -extrudeDepth / 2]}>
<extrudeGeometry args={[shape, { depth: extrudeDepth, bevelEnabled: false }]} />
<meshStandardMaterial
color={new THREE.Color().setHSL(index / total, 1, 0.5)}
metalness={0.8}
roughness={0.2}
/>
</mesh>
</group>
);
};
const Scene = ({ radius, verticalOffset = 0 }: { radius: number, verticalOffset?: number }) => {
const gearRadius = radius * Math.sin(Math.PI / GEAR_COUNT) * 1.15;
const groupRef = useRef<THREE.Group>(null);
const { viewport } = useThree();
const tilt = Math.PI / 3;
const responsiveScale = useMemo(() => {
const outerR = radius + gearRadius;
// Width is determined by the horizontal diameter of the ring
const projectedW = outerR * 2;
// Height is determined by the tilted ring diameter PLUS the gear thickness
// which can stand 'upright' due to the Mobius twist.
const projectedH = (radius * 2 * Math.cos(tilt)) + (gearRadius * 2);
const scaleX = viewport.width / projectedW;
const scaleY = viewport.height / projectedH;
// Increase multiplier to 0.85 to fill more space while keeping a safe margin
return Math.min(scaleX, scaleY) * 0.85;
}, [viewport.width, viewport.height, radius, gearRadius, tilt]);
useFrame((state, delta) => {
if (groupRef.current) groupRef.current.rotation.z += delta * 0.2;
});
return (
<group position={[0, verticalOffset * viewport.height, 0]} rotation={[-tilt, 0, 0]} scale={responsiveScale}>
<group ref={groupRef}>
{Array.from({ length: GEAR_COUNT }).map((_, i) => (
<Gear key={i} index={i} total={GEAR_COUNT} radius={radius} gearRadius={gearRadius} teeth={TEETH_PER_GEAR} />
))}
</group>
</group>
);
};
const App = () => {
return (
<>
<Canvas dpr={[1, 2]} gl={{ antialias: true, alpha: true }}>
{/* High distance and low FOV approximates an orthographic camera,
eliminating perspective centering issues where the front of the ring
looks larger and lower than the back. */}
<PerspectiveCamera makeDefault position={[0, 0, 50]} fov={5.5} />
<ambientLight intensity={2.5} color="#ffffff" />
<directionalLight position={[4, 6, 5]} intensity={3} color="#fff0dd" />
<directionalLight position={[-3, -2, -3]} intensity={1.2} color="#6688cc" />
<Scene radius={1} verticalOffset={0.05} />
</Canvas>
<div className="description">
Gears on a Möbius strip create an illusion where because the entire strip is rotating, alternating gears seem to be turning faster/slower. Concept courtesy of @prof_g
</div>
</>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
```
**styles.css**
```css
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: #000000;
}
.description {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 600px;
text-align: center;
font-family: system-ui, -apple-system, sans-serif;
font-size: 16px;
line-height: 1.4;
color: rgba(255, 255, 255, 0.7);
pointer-events: none;
z-index: 100;
text-wrap: balance;
}
```
**index.html**
```html
<div id="root"></div>
```
**data.txt**
```txt
```
**infer.md**
```md
```
**insight.json**
```json
```
**config.json**
```json
{
"description": "Gears on a mobius strip create an illusion where because the entire strip is rotating, alternating gears seem to be turning faster/slower.",
"dependencies": {
"three": "^0.165.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"@react-three/fiber": "^9.5.0",
"@react-three/drei": "^10.7.7"
}
}
```