Mobius progress - man walking the Möbius strip
---
format: typebulb/v1
name: Mobius Progress
---
**code.tsx**
```tsx
import React, { useMemo, useRef, useState } 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 STRIP_RADIUS = 1.8;
const STRIP_WIDTH = 0.5;
const ROT_SPEED = 0.5; // rad/sec
const START_ANGLE = -Math.PI / 2; // 6 o'clock position
const RUNNER_FOOT_LIFT = 0.46; // offset along surface normal so the feet sit on the strip
type RunnerMode = "walk" | "stand";
function mobiusPos(u: number, v: number) {
const x = (STRIP_RADIUS + v * Math.cos(u / 2)) * Math.cos(u);
const z = (STRIP_RADIUS + v * Math.cos(u / 2)) * Math.sin(u);
const y = v * Math.sin(u / 2);
return new THREE.Vector3(x, y, z);
}
function mobiusDerivatives(u: number, v: number) {
// p(u,v):
// x = (R + v cos(u/2)) cos u
// z = (R + v cos(u/2)) sin u
// y = v sin(u/2)
const A = STRIP_RADIUS + v * Math.cos(u / 2);
const dAdu = -0.5 * v * Math.sin(u / 2);
const dxdu = dAdu * Math.cos(u) + A * -Math.sin(u);
const dzdu = dAdu * Math.sin(u) + A * Math.cos(u);
const dydu = 0.5 * v * Math.cos(u / 2);
const dAdv = Math.cos(u / 2);
const dxdv = dAdv * Math.cos(u);
const dzdv = dAdv * Math.sin(u);
const dydv = Math.sin(u / 2);
return {
dpdu: new THREE.Vector3(dxdu, dydu, dzdu),
dpdv: new THREE.Vector3(dxdv, dydv, dzdv),
};
}
const MobiusStrip = ({ dotsEnabled }: { dotsEnabled: boolean }) => {
const materialRef = useRef<THREE.ShaderMaterial>(null);
const geometry = useMemo(() => {
const geo = new THREE.BufferGeometry();
const vertices: number[] = [];
const uVals: number[] = [];
const vVals: number[] = [];
const indices: number[] = [];
const uSegments = 400;
const vSegments = 40;
for (let i = 0; i <= uSegments; i++) {
const u = (i / uSegments) * Math.PI * 2;
for (let j = 0; j <= vSegments; j++) {
const v = (j / vSegments - 0.5) * STRIP_WIDTH;
const x = (STRIP_RADIUS + v * Math.cos(u / 2)) * Math.cos(u);
const z = (STRIP_RADIUS + v * Math.cos(u / 2)) * Math.sin(u);
const y = v * Math.sin(u / 2);
vertices.push(x, y, z);
uVals.push(u);
vVals.push(v);
}
}
for (let i = 0; i < uSegments; i++) {
for (let j = 0; j < vSegments; j++) {
const row1 = i * (vSegments + 1);
const row2 = (i + 1) * (vSegments + 1);
const a = row1 + j;
const b = row2 + j;
const c = row2 + j + 1;
const d = row1 + j + 1;
indices.push(a, b, d);
indices.push(b, c, d);
}
}
geo.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
geo.setAttribute("uVal", new THREE.Float32BufferAttribute(uVals, 1));
geo.setAttribute("vVal", new THREE.Float32BufferAttribute(vVals, 1));
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
}, []);
const uniforms = useMemo(() => ({
colorRed: { value: new THREE.Color(0xff2222) },
colorBlue: { value: new THREE.Color(0x2222ff) },
stripWidth: { value: STRIP_WIDTH },
dotsEnabled: { value: dotsEnabled ? 1.0 : 0.0 }
}), []);
// Sync dotsEnabled state to uniform
useFrame(() => {
if (materialRef.current) {
materialRef.current.uniforms.dotsEnabled.value = dotsEnabled ? 1.0 : 0.0;
}
});
return (
<mesh geometry={geometry}>
<shaderMaterial
ref={materialRef}
side={THREE.DoubleSide}
uniforms={uniforms}
vertexShader={`
varying float vU;
varying float vV;
attribute float uVal;
attribute float vVal;
void main() {
vU = uVal;
vV = vVal;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`}
fragmentShader={`
varying float vU;
varying float vV;
uniform vec3 colorRed;
uniform vec3 colorBlue;
uniform float stripWidth;
uniform float dotsEnabled;
void main() {
float u = vU;
float phase = gl_FrontFacing ? 0.0 : 3.14159265;
float t = cos(u * 0.5 + phase);
vec3 color = mix(colorBlue, colorRed, t * 0.5 + 0.5);
float s = vU / (6.28318530718);
float w = (vV / stripWidth) + 0.5;
float holeCount = 28.0;
float ds = abs(fract(s * holeCount) - 0.5);
// Two side-by-side holes per "station" so the runner can plausibly stay on the centerline.
// Centers are offset from the middle, leaving a solid bridge at w ~= 0.5.
float holeOffset = 0.23; // separation from center (in normalized width 0..1)
float holeHalfW = 0.14; // hole radius across width
float holeHalfU = 0.17; // hole radius along u
float dw1 = abs(w - (0.5 - holeOffset));
float dw2 = abs(w - (0.5 + holeOffset));
vec2 q1 = vec2(ds / holeHalfU, dw1 / holeHalfW);
vec2 q2 = vec2(ds / holeHalfU, dw2 / holeHalfW);
float d = min(length(q1), length(q2));
float hole = 1.0 - smoothstep(1.0, 1.10, d);
if (dotsEnabled > 0.5 && hole > 0.5) discard;
gl_FragColor = vec4(color, 1.0);
}
`}
/>
</mesh>
);
};
const Runner = ({
enabled,
mode,
rotYRef,
}: {
enabled: boolean;
mode: RunnerMode;
rotYRef: React.MutableRefObject<number>;
}) => {
const groupRef = useRef<THREE.Group>(null);
const tRef = useRef(0);
const prevModeRef = useRef<RunnerMode>(mode);
const uTravelRef = useRef<number>(START_ANGLE); // used in "walk" mode (advances at ROT_SPEED to cancel group rotation)
const uStandRef = useRef<number>(START_ANGLE); // used in "stand" mode (fixed on strip)
const innerRef = useRef<THREE.Group>(null);
const leftArmRef = useRef<THREE.Group>(null);
const rightArmRef = useRef<THREE.Group>(null);
const leftLegRef = useRef<THREE.Group>(null);
const rightLegRef = useRef<THREE.Group>(null);
const leftShinRef = useRef<THREE.Group>(null);
const rightShinRef = useRef<THREE.Group>(null);
const basisMat = useMemo(() => new THREE.Matrix4(), []);
const quat = useMemo(() => new THREE.Quaternion(), []);
const tmpRight = useMemo(() => new THREE.Vector3(), []);
const tmpUp = useMemo(() => new THREE.Vector3(), []);
const tmpForward = useMemo(() => new THREE.Vector3(), []);
useFrame((state, delta) => {
if (!enabled || !groupRef.current) return;
// Initialize refs on first frame (ensures continuity even if rotY already advanced a bit)
if (tRef.current === 0 && prevModeRef.current === mode) {
uStandRef.current = START_ANGLE;
uTravelRef.current = START_ANGLE;
}
// Handle mode transitions without teleporting:
// - walk -> stand: freeze at the current evaluated u (uTravel - rotY)
// - stand -> walk: pick uTravel so evaluated u stays the same after applying -rotY
if (mode !== prevModeRef.current) {
if (prevModeRef.current === "walk" && mode === "stand") {
uStandRef.current = uTravelRef.current;
} else if (prevModeRef.current === "stand" && mode === "walk") {
uTravelRef.current = uStandRef.current;
}
prevModeRef.current = mode;
}
if (mode === "walk") {
tRef.current += delta;
uTravelRef.current += ROT_SPEED * delta;
}
// walk: advances u at ROT_SPEED so that (u - rotY) stays constant in world space
// stand: stays on the same patch of strip (and rotates with it)
const u = mode === "walk" ? uTravelRef.current : uStandRef.current;
const v = 0; // centerline
const pos = mobiusPos(u, v);
const { dpdu, dpdv } = mobiusDerivatives(u, v);
tmpForward.copy(dpdu).normalize();
const widthDir = dpdv.clone().normalize();
tmpUp.crossVectors(tmpForward, widthDir).normalize(); // surface normal (will flip after 1 loop)
tmpRight.crossVectors(tmpUp, tmpForward).normalize();
basisMat.makeBasis(tmpRight, tmpUp, tmpForward);
quat.setFromRotationMatrix(basisMat);
// Lift the runner so the feet (not the torso) sit on the surface
pos.addScaledVector(tmpUp, RUNNER_FOOT_LIFT);
groupRef.current.position.copy(pos);
groupRef.current.quaternion.copy(quat);
if (mode === "stand") {
// Reset to a neutral pose (prevents freezing mid-stride when switching modes)
if (leftLegRef.current) leftLegRef.current.rotation.x = 0;
if (rightLegRef.current) rightLegRef.current.rotation.x = 0;
if (leftShinRef.current) leftShinRef.current.rotation.x = 0;
if (rightShinRef.current) rightShinRef.current.rotation.x = 0;
if (leftArmRef.current) leftArmRef.current.rotation.x = 0;
if (rightArmRef.current) rightArmRef.current.rotation.x = 0;
if (innerRef.current) {
innerRef.current.position.y = 0;
innerRef.current.rotation.x = 0;
}
return;
}
// Cheap run-cycle (walk mode)
const phase = tRef.current * 10.0;
const swing = Math.sin(phase) * 0.85;
const armSwing = -swing * 0.75;
// Legs swing from hip
if (leftLegRef.current) leftLegRef.current.rotation.x = swing;
if (rightLegRef.current) rightLegRef.current.rotation.x = -swing;
// Bend knee a bit on the "back" leg (simple heuristic)
const leftKnee = Math.max(0, -Math.sin(phase)) * 1.1;
const rightKnee = Math.max(0, Math.sin(phase)) * 1.1;
if (leftShinRef.current) leftShinRef.current.rotation.x = leftKnee;
if (rightShinRef.current) rightShinRef.current.rotation.x = rightKnee;
// Arms swing opposite to legs
if (leftArmRef.current) leftArmRef.current.rotation.x = armSwing;
if (rightArmRef.current) rightArmRef.current.rotation.x = -armSwing;
// Slight bob + forward lean
if (innerRef.current) {
innerRef.current.position.y = Math.sin(phase * 2.0) * 0.01;
innerRef.current.rotation.x = 0.25; // lean forward
}
});
if (!enabled) return null;
return (
<group ref={groupRef}>
<group ref={innerRef}>
{/* Torso */}
<mesh position={[0, 0.02, 0]}>
<capsuleGeometry args={[0.07, 0.22, 8, 12]} />
<meshBasicMaterial color={0x2fbf71} />
</mesh>
{/* Pelvis */}
<mesh position={[0, -0.12, 0]}>
<boxGeometry args={[0.12, 0.08, 0.07]} />
<meshBasicMaterial color={0x2f3a4a} />
</mesh>
{/* Neck */}
<mesh position={[0, 0.17, 0]}>
<cylinderGeometry args={[0.02, 0.02, 0.04, 10]} />
<meshBasicMaterial color={0xffd2b8} />
</mesh>
{/* Head */}
<mesh position={[0, 0.23, 0]}>
<sphereGeometry args={[0.06, 18, 18]} />
<meshBasicMaterial color={0xffd2b8} />
</mesh>
{/* Hair cap */}
<mesh position={[0, 0.255, 0]}>
<sphereGeometry args={[0.062, 18, 18, 0, Math.PI * 2, 0, Math.PI * 0.55]} />
<meshBasicMaterial color={0x7a5a3a} />
</mesh>
{/* Arms (as pivoting groups at shoulders) */}
<group ref={leftArmRef} position={[-0.095, 0.115, 0]}>
{/* upper arm */}
<mesh position={[0, -0.06, 0]}>
<cylinderGeometry args={[0.018, 0.02, 0.12, 10]} />
<meshBasicMaterial color={0xe9eef6} />
</mesh>
{/* forearm */}
<mesh position={[0, -0.135, 0]}>
<cylinderGeometry args={[0.016, 0.017, 0.10, 10]} />
<meshBasicMaterial color={0xffd2b8} />
</mesh>
{/* hand */}
<mesh position={[0, -0.19, 0]}>
<sphereGeometry args={[0.018, 12, 12]} />
<meshBasicMaterial color={0xffd2b8} />
</mesh>
</group>
<group ref={rightArmRef} position={[0.095, 0.115, 0]}>
<mesh position={[0, -0.06, 0]}>
<cylinderGeometry args={[0.02, 0.018, 0.12, 10]} />
<meshBasicMaterial color={0xe9eef6} />
</mesh>
<mesh position={[0, -0.135, 0]}>
<cylinderGeometry args={[0.017, 0.016, 0.10, 10]} />
<meshBasicMaterial color={0xffd2b8} />
</mesh>
<mesh position={[0, -0.19, 0]}>
<sphereGeometry args={[0.018, 12, 12]} />
<meshBasicMaterial color={0xffd2b8} />
</mesh>
</group>
{/* Legs (hip pivot groups) */}
<group ref={leftLegRef} position={[-0.045, -0.155, 0.015]}>
{/* thigh */}
<mesh position={[0, -0.065, 0]}>
<boxGeometry args={[0.04, 0.13, 0.045]} />
<meshBasicMaterial color={0x2f3a4a} />
</mesh>
{/* shin pivot at knee */}
<group ref={leftShinRef} position={[0, -0.13, 0]}>
<mesh position={[0, -0.06, 0.005]}>
<boxGeometry args={[0.036, 0.12, 0.04]} />
<meshBasicMaterial color={0x2f3a4a} />
</mesh>
{/* foot */}
<mesh position={[0, -0.13, 0.035]}>
<boxGeometry args={[0.05, 0.03, 0.09]} />
<meshBasicMaterial color={0xf4b400} />
</mesh>
</group>
</group>
<group ref={rightLegRef} position={[0.045, -0.155, 0.015]}>
<mesh position={[0, -0.065, 0]}>
<boxGeometry args={[0.04, 0.13, 0.045]} />
<meshBasicMaterial color={0x2f3a4a} />
</mesh>
<group ref={rightShinRef} position={[0, -0.13, 0]}>
<mesh position={[0, -0.06, 0.005]}>
<boxGeometry args={[0.036, 0.12, 0.04]} />
<meshBasicMaterial color={0x2f3a4a} />
</mesh>
<mesh position={[0, -0.13, 0.035]}>
<boxGeometry args={[0.05, 0.03, 0.09]} />
<meshBasicMaterial color={0xf4b400} />
</mesh>
</group>
</group>
</group>
</group>
);
};
const Scene = ({
dotsEnabled,
runnerEnabled,
runnerMode,
}: {
dotsEnabled: boolean;
runnerEnabled: boolean;
runnerMode: RunnerMode;
}) => {
const { viewport } = useThree();
const tilt = Math.PI / 6; // 30 degrees
const stripGroupRef = useRef<THREE.Group>(null);
const rotYRef = useRef(0);
const responsiveScale = useMemo(() => {
// Math inspired by the Mobius Gear demo for perfect framing
const outerR = STRIP_RADIUS + (STRIP_WIDTH / 2);
const projectedW = outerR * 2.2;
const projectedH = (STRIP_RADIUS * 2 * Math.cos(tilt)) + (STRIP_WIDTH * 2);
const scaleX = viewport.width / projectedW;
const scaleY = viewport.height / projectedH;
return Math.min(scaleX, scaleY);
}, [viewport.width, viewport.height, tilt]);
useFrame((state, delta) => {
rotYRef.current += delta * ROT_SPEED;
if (stripGroupRef.current) stripGroupRef.current.rotation.y = rotYRef.current;
});
return (
<group rotation={[tilt, 0, 0]} scale={responsiveScale}>
<group ref={stripGroupRef}>
<MobiusStrip dotsEnabled={dotsEnabled} />
<Runner
enabled={runnerEnabled}
mode={runnerMode}
rotYRef={rotYRef}
/>
</group>
</group>
);
};
const App = () => {
const [dotsEnabled, setDotsEnabled] = useState(true);
const [runnerEnabled, setRunnerEnabled] = useState(true);
const [runnerMode, setRunnerMode] = useState<RunnerMode>("walk");
return (
<div className="container">
<Canvas dpr={[1, 2]} gl={{ antialias: true, alpha: true }}>
{/* Orthographic-like perspective behavior */}
<PerspectiveCamera makeDefault position={[0, 0, 50]} fov={5.5} />
<ambientLight intensity={0.7} />
<Scene
dotsEnabled={dotsEnabled}
runnerEnabled={runnerEnabled}
runnerMode={runnerMode}
/>
</Canvas>
<div className="ui">
<button className="toggle" onClick={() => setDotsEnabled(!dotsEnabled)}>
{dotsEnabled ? "Holey" : "Smooth"}
</button>
<button className="toggle" onClick={() => setRunnerEnabled(!runnerEnabled)}>
{runnerEnabled ? "With Man" : "Strip Only"}
</button>
<button
className="toggle"
onClick={() => setRunnerMode((m) => (m === "walk" ? "stand" : "walk"))}
disabled={!runnerEnabled}
>
{runnerMode === "walk" ? "Walking" : "Standing"}
</button>
</div>
</div>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
```
**styles.css**
```css
:root {
--bg-color: #050508;
--ui-bg: rgba(255, 255, 255, 0.70);
--ui-fg: rgba(0, 0, 0, 0.85);
--ui-border: rgba(0, 0, 0, 0.18);
}
html[data-theme="dark"] {
--ui-bg: rgba(0, 0, 0, 0.45);
--ui-fg: rgba(255, 255, 255, 0.92);
--ui-border: rgba(255, 255, 255, 0.16);
}
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background-color: var(--bg-color);
}
#root {
width: 100vw;
height: 100vh;
}
.container {
width: 100%;
height: 100%;
position: relative;
background: radial-gradient(circle at center, #101015 0%, #050508 100%);
}
.ui {
position: absolute;
top: 12px;
left: 12px;
z-index: 10;
display: flex;
flex-direction: row;
gap: 8px;
}
.toggle {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--ui-border);
background: var(--ui-bg);
color: var(--ui-fg);
cursor: pointer;
font-family: system-ui, -apple-system, sans-serif;
font-size: 13px;
font-weight: 500;
backdrop-filter: blur(8px);
transition: all 0.2s ease;
}
.toggle:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.toggle:hover {
background: var(--ui-fg);
color: var(--bg-color);
}
.toggle:active {
transform: translateY(1px);
}
```
**index.html**
```html
<div id="root"></div>
```
**config.json**
```json
{
"description": "Mobius progress - man walking the Möbius strip",
"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"
}
}
```